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")))
}
}
})
}
}
Related
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 2 years ago.
Improve this question
I have this ugly massive piece of code. It does many things, mostly around querying databases. Each query depends on the result of the previous query or on the parameters of the incoming request body. Queries are done asynchronously using Future. The code works but is not readable. How can I restructure it to make it concise? I have thought of using for instead of map and flatMap but due to dependencies on previous queries, I am not able to figure out how to use result of previous Future in new ones and how to handle error paths.
I am using play 2.6 and scala 2.12.2
def oldSignupUser:Action[AnyContent] = silhouette.UserAwareAction.async {
implicit request => {
val body: AnyContent = request.body
val jsonBody: Option[JsValue] = body.asJson
jsonBody match {
case Some(json) => {
val userProfile: Option[UserProfile] = json.asOpt[UserProfile] userProfile match {
case Some(profile) => {
val loginInfo = LoginInfo(CredentialsProvider.ID, profile.externalProfileDetails.email)
val userKeys = UserKeys(utilities.bucketIDFromEmail(profile.externalProfileDetails.email),profile.externalProfileDetails.email,loginInfo,profile.externalProfileDetails.firstName,profile.externalProfileDetails.lastName)
val findUserFuture: Future[Option[User]] = userRepo.findOne(userKeys) // userFuture will eventually contain the result of database query i.e Some(user) or None
findUserFuture.flatMap { (userOption: Option[User]) =>
userOption match {
case Some(user) => {
if(user.profile.internalProfileDetails.get.confirmed) {
Future {
Ok(Json.toJson(JsonResultError(messagesApi("error.duplicateUser")(langs.availables(0)))))
}
} else {
val userKeys = UserKeys(utilities.bucketIDFromEmail(user.profile.externalProfileDetails.email),user.profile.externalProfileDetails.email,loginInfo,user.profile.externalProfileDetails.firstName,user.profile.externalProfileDetails.lastName)
val userToken:UserToken = utilities.createUserToken(user.id,userKeys,UserTokenType.RegistrationConfirmation)
val userTokenSaveFuture:Future[Option[UserToken]] = userTokenRepo.save(userToken)
logger.trace(s"user token save future ${userTokenSaveFuture}")
userTokenSaveFuture.map( (userTokenOption:Option[UserToken])=>{
userTokenOption match {
case Some(userToken) => {
val signupEmailOption:Option[SignupEmail] = createEmailMessageForUserToken(userToken)
signupEmailOption match {
case Some(signupEmail:SignupEmail) =>{
val _:String = mailerService.sendEmail(signupEmail.subject, signupEmail.from,List(user.profile.externalProfileDetails.email),None,Some(signupEmail.html))
Ok(Json.toJson(JsonResultSuccess(messagesApi("success.userSignupConfirmationEmailSent")(langs.availables(0)))))
}
case None =>{
InternalServerError(Json.toJson(JsonResultError("Internal Server Error")))
}
}
}
case None => {
Ok(Json.toJson(JsonResultError("user not added"))) //Todom - this is misleading as user is added but token isn't
}
}
})
}
}
case None => {
val passwordInfo:PasswordInfo = userRepo.hashPassword(profile.externalProfileDetails.password.get)
val bucketId = utilities.bucketIDFromEmail(profile.externalProfileDetails.email)
val newUser:User = User(
utilities.getUniqueID(),//UUID.randomUUID(),
UserProfile(Some(InternalUserProfile(loginInfo,bucketId,false,Some(passwordInfo))),
profile.externalProfileDetails))
val saveUserFuture:Future[Option[User]] = userRepo.save(newUser)
saveUserFuture.flatMap { (userOption:Option[User]) =>{ userOption match {
case Some(user) => {
val initialPortfolio = user.profile.externalProfileDetails.portfolio
val profileAndPortfolio = profile.externalProfileDetails.copy(portfolio = initialPortfolio)
logger.trace(s"saving external profile and portfolio ${profileAndPortfolio}")
val savedProfileAndPortfolioOptionFuture = userProfileAndPortfolioRepo.save(profileAndPortfolio)
savedProfileAndPortfolioOptionFuture.flatMap(profileAndPortfolioOption =>{
profileAndPortfolioOption match {
case Some(profileAndPortfolio) => {
val userKeys = UserKeys(utilities.bucketIDFromEmail(user.profile.externalProfileDetails.email),user.profile.externalProfileDetails.email,loginInfo,user.profile.externalProfileDetails.firstName,user.profile.externalProfileDetails.lastName)
val userToken:UserToken = utilities.createUserToken(user.id,userKeys,UserTokenType.RegistrationConfirmation)
val userTokenSaveFuture:Future[Option[UserToken]] = userTokenRepo.save(userToken)
userTokenSaveFuture.flatMap( (userTokenOption:Option[UserToken])=>{
userTokenOption match {
case Some(userToken) => {
val signupEmailOption = createEmailMessageForUserToken(userToken)
signupEmailOption match {
case Some(signupEmail) =>{
val _:String = mailerService.sendEmail(signupEmail.subject,signupEmail.from,List(user.profile.externalProfileDetails.email),None,Some(signupEmail.html))
Future{Ok(Json.toJson(JsonResultSuccess((messagesApi("success.userSignupConfirmationEmailSent"))(langs.availables(0)))))}
}
case None =>{
Future{InternalServerError(Json.toJson(JsonResultError("Internal Server Error")))}
}
}
}
case None => {
Future{Ok(Json.toJson(JsonResultError("user not added"))) }
}
}
})
}
case None =>{
Future{InternalServerError(Json.toJson(JsonResultError("Internal Server Error")))}
}
}
})
}
case None => {
Future{Ok(Json.toJson(JsonResultError("unable to add user")))}
}
}
}
}
.recover { case x => {
x match {
case _:EmailException =>InternalServerError(Json.toJson(JsonResultError("The server encountered internal error and couldn't sent email to the email id.")))
case _ => InternalServerError(Json.toJson(JsonResultError("Internal Server Error")))
}
}
}
}
}
}
.recover { case x => {
logger.trace("Future failed in signupUser. In recover. Returning Internal Server Error"+x)
x match {
case _:EmailException =>InternalServerError(Json.toJson(JsonResultError("The server encountered internal error and couldn't sent email to the email id.")))
case _ => InternalServerError(Json.toJson(JsonResultError("Internal Server Error")))
}
}
}
}
case None => Future {
logger.trace("invalid profile structure")
Ok(Json.toJson(JsonResultError(messagesApi("error.incorrectBodyStructure")(langs.availables(0))))) }
}
}
case None => Future {
Ok(Json.toJson(JsonResultError(messagesApi("error.incorrectBodyType")(langs.availables(0))))) }
}
}
}
Update - the question was closed before I could solve the problem. Many thanks to mfirry and tim for their answers. this is the version I could come up with which I believe is modular.
def getUserProfileFromBody(json:JsValue): Option[UserProfile] ={
val userProfile = json.asOpt[UserProfile] //check if json conforms with UserProfile structure
userProfile
}
def getJsonBody(body:AnyContent) = {
val jsonBody: Option[JsValue] = body.asJson
jsonBody
}
def generateUserKeysFromUserProfile(profile:UserProfile):UserKeys = {
val loginInfo = LoginInfo(/*provider id eg "credentials"*/CredentialsProvider.ID, /*provider Key eg email*/profile.externalProfileDetails.email)
val userKeys = UserKeys(utilities.bucketIDFromEmail(profile.externalProfileDetails.email),profile.externalProfileDetails.email,loginInfo,profile.externalProfileDetails.firstName,profile.externalProfileDetails.lastName)
logger.trace(s"generated userkey ${userKeys}")
userKeys
}
def findIfUserIsNewOrExisting(userKey:UserKeys): Future[Boolean] ={
logger.trace(s"looking for user with keys ${userKey}")
val findUserFuture = userRepo.findOne(userKey) // userFuture will eventually contain the result of database query i.e Some(user) or None
logger.trace(s"user future is ${findUserFuture}")
for(userOption <- findUserFuture) yield {
logger.trace(s"user option ${userOption}")
userOption match {
case Some(user) => {
val userConfirmed = isUserConfirmed(user)
if(userConfirmed) {
throw new DuplicateUserException(user,"duplicateuser", new Throwable("duplicateuser"))
} else throw new UnconfirmedUserException(user,"unconfirmeduser", new Throwable("unconfirmeduser"))
}
case None => {
false
}
}
}
}
def returnError(error:String) = {
println(s"returning error ${error}")
Future {
/*
note the distintion between langs and messages. Lang means languages this application supports
eg English, French
Messages are the messages defined per language. Eg app.title is a message defined inn English. It might not be defined in French
*/
/*logger.trace("langs array"+langs.availables) //languages available
logger.trace("app.title: "+messagesApi.isDefinedAt("app.title")(langs.availables(0)))//pick the first language and see taht app.title is defined in it
logger.trace("error: "+messagesApi.isDefinedAt("error.incorrectBodyType")(langs.availables(0)))//see taht error.incorrectBodyType is defined in the 1st language
*/
Ok(Json.toJson(JsonResultError(error)))
}/*TODOM - Standardise error messages. Use as constants*/
}
def isUserConfirmed(user:User):Boolean = user.profile.internalProfileDetails.get.confirmed
def sendConfirmationTokenForUser(user:User) = {
val userKeys = generateUserKeysFromUserProfile(user.profile)
//for this user, create a token which could be sent in the email for verification
val userToken:UserToken = utilities.createUserToken(user.id,userKeys,UserTokenType.RegistrationConfirmation)
logger.trace(s"saving token ${userToken}")
val userTokenSaveFuture:Future[Option[UserToken]] = userTokenRepo.save(userToken)
logger.trace(s"user token save future ${userTokenSaveFuture}")
for(userTokenOption <- userTokenSaveFuture) yield {
logger.trace(s"user token ${userTokenOption}")
userTokenOption match {
case Some(userToken) => {
val signupEmailOption:Option[SignupEmail] = createEmailMessageForUserToken(userToken)
signupEmailOption match {
case Some(signupEmail:SignupEmail) =>{
val email = mailerService.sendEmail(signupEmail.subject, signupEmail.from,List(user.profile.externalProfileDetails.email),None,Some(signupEmail.html))
println(s"sent email message ${email}")
email
//Ok(Json.toJson(JsonResultSuccess(messagesApi("success.userSignupConfirmationEmailSent")(langs.availables(0)))))
}
case None =>{
logger.trace("unable to create html response for email confirmation")
//InternalServerError(Json.toJson(JsonResultError("Internal Server Error")))
throw EmailMessageCreationException("errorInCreatingHTML", new Throwable("errorInCreatingHTML"))
}
}
}
case None => {
logger.trace("error in adding token")
throw EmailTokenSaveException("emailTokenSaveException", new Throwable("emailTokenSaveException"))
//Ok(Json.toJson(JsonResultError("user not added"))) //Todom - this is misleading as user is added but token isn't
}
}
}
}
def addUserToDatabase(user:User) = {
println(s"saving user ${user}")
val saveUserFuture:Future[Option[User]] = userRepo.save(user)
for(userOption <- saveUserFuture) yield {
userOption match {
case Some(user) => {
logger.trace("user added successfully "+user)
user
}
case None => throw new NewUserAdditionException("newUserAdditionError", new Throwable("newUserAdditionError"))
}
}
}
def addNewUser(profile:UserProfile) = {
logger.trace(s"new user sign up request with profile ${profile}")
////NOTE - //salt is empty for BCryptSha256PasswordHasher. The 'hash' method of BCryptSha256PasswordHasher does not return the salt separately because it is embedded in the hashed password.
//should creation of passwordInfo be moved to UserRepo? Not sure.
//A profile associated with the credentials provider stores a Silhouette PasswordInfo object holding the hashed password
//val passwordInfo:PasswordInfo = userRepo.passwordHasher.hash(profile.externalProfileDetails.password.get)
val passwordInfo: PasswordInfo = userRepo.hashPassword(profile.externalProfileDetails.password.get)
//logger.trace("password info is ",passwordInfo)
val bucketId = utilities.bucketIDFromEmail(profile.externalProfileDetails.email)
val loginInfo = LoginInfo(/*provider id eg "credentials"*/ CredentialsProvider.ID, /*provider Key eg email*/ profile.externalProfileDetails.email)
val newUser: User = User(
utilities.getUniqueID(), //UUID.randomUUID(),
UserProfile(Some(InternalUserProfile(loginInfo, bucketId, false, Some(passwordInfo))),
profile.externalProfileDetails))
logger.trace("adding new user" + newUser)
addUserToDatabase(newUser)
}
def saveProfileAndPortfolio(profileAndPortfolio:ExternalUserProfile) = {
logger.trace(s"saving external profile and portfolio ${profileAndPortfolio}")
val savedProfileAndPortfolioOptionFuture = userProfileAndPortfolioRepo.save(profileAndPortfolio)
for(savedProfileAndPortfolio <- savedProfileAndPortfolioOptionFuture) yield {
savedProfileAndPortfolio match {
case Some(profileAndPortfolio) => profileAndPortfolio
case None => throw ProfileAndPortfolioAdditionException("profileAndPortfolioAdditionException",new Throwable("profileAndPortfolioAdditionException"))
}
}
}
def createUserProfileAndPortfolioInformation(user:User) = {
val profile = user.profile
val initialPortfolio = user.profile.externalProfileDetails.portfolio //Some(TagsOfInterestToAUserAPI(Set(),Set(),Set()))
val profileAndPortfolio = profile.externalProfileDetails.copy(portfolio = initialPortfolio)
saveProfileAndPortfolio(profileAndPortfolio)
}
def signupUser = silhouette.UserAwareAction.async {
implicit request => {
logger.trace(s"received request ${request}")
val jsonBody = getJsonBody(request.body)
/*
TODOM - testcase - check what happens if some other body type is sent.
*/
jsonBody match {
case Some(json) => { //got json in message body.
//TODOM - convert to pretty print only if logger level is trace
val readableString: String = Json.prettyPrint(json)
logger.trace(s"received Json ${readableString}")
val userProfile = getUserProfileFromBody(json)
userProfile match {
case Some(profile) => { //json conforms to UserProfile.
logger.trace(s"received correct profile structure ${profile}")
val userKeys = generateUserKeysFromUserProfile(profile)
val res = for{isNewUser <- findIfUserIsNewOrExisting(userKeys) //this will throw error if user is duplicate
newUserDetails <- addNewUser(profile)
profileAndPortfolioInfo <- createUserProfileAndPortfolioInformation(newUserDetails)
confirmationEmail <- sendConfirmationTokenForUser(newUserDetails)} yield {
Ok(Json.toJson(JsonResultSuccess((messagesApi("success.userSignupConfirmationEmailSent"))(langs.availables(0)))))
}
res.recover {
case exception: DuplicateUserException => Ok(Json.toJson(JsonResultError(messagesApi("error.duplicateUser")(langs.availables(0)))))
case exception: UnconfirmedUserException => {
sendConfirmationTokenForUser(exception.user)
Ok(Json.toJson(JsonResultSuccess((messagesApi("success.userSignupConfirmationEmailSent"))(langs.availables(0)))))
}
case _:EmailException =>InternalServerError(Json.toJson(JsonResultError("The server encountered internal error and couldn't sent email to the email id.")))
case exception:ProfileAndPortfolioAdditionException => Ok(Json.toJson(JsonResultError("user not added")))
case _:EmailMessageCreationException => Ok(Json.toJson(JsonResultError("user not added")))
case _: EmailTokenSaveException=> Ok(Json.toJson(JsonResultError("user not added")))
case x => {
logger.error(s"unknown exception ${x}")
InternalServerError(Json.toJson(JsonResultError("Internal Server Error")))
}
}
res
}
//Json doesn't conform to UserProfile
case None => Future {
logger.trace("invalid profile structure")
Ok(Json.toJson(JsonResultError(messagesApi("error.incorrectBodyStructure")(langs.availables(0))))) } /*TODOM - Standardise error messages. Use as constants*/
}
}
//message body is not json. Error.
//TODOM - langs contains array of lang. picking the 1st one but would need too pick based on locale.
//langs.availables(0) maps to array defined in application.conf eg. - play.i18n.langs = [ "en", "en-US", "fr" ]
case None =>{
logger.trace("incorrect body type")
returnError(messagesApi("error.incorrectBodyType")(langs.availables(0)))
}
}
}
}
The most obvious change is to put code in functions. By breaking up the logic into functions with meaningful names you make the code much clearer to read. If you define them as local functions within the method they can access context from the method without it having to be passed in as parameters.
Put all the error returns in static vals outside the method rather than creating them each time.
Consider using Option.fold rather than match:
option.fold(error){ result =>
// Further code
}
Remove the {} after case x =>, they are not required.
The first case in case x => { x match { is redundant.
And don't worry about performance until you can prove that the performance of this section of code has a material effect on the behaviour of the application.
One problem is that you are not using the Failure case of Future to indicate failure so you can't use map/flatMap to chain operations that might fail.
Create your own subclass of Exception and have the methods return Future.failed(MyException(error)) on failure rather than Future(error)
Then do this
val res =
for {
res1 <- futureAction1
res2 <- futureAction2(res1)
res3 <- futureAction3(res2)
} yield {
res3
}
and finally
res.recover{ case MyException(err) => err }
The for will stop at the first Failure return value, and the recover will turn this into the appropriate Success value.
Here is the situation
// validateRoute act like a directive which validate a route before proceeding further
override def validateRoute(route: Route)(implicit ec: ExecutionContext): Route = {
extractRequest { request =>
decodeRequest {
entity(as[String]) { content =>
(headerValueByName("header1") & headerValueByName("header2")) {
case (header1, header2) => {
// dom some
// validate() returns a Future[Either[Error, Boolean]]
validate().map {
result => {
result match {
case Right(_) => route
case Left(ex) => complete(StatusCodes.Unauthorized -> ex.getMessage)
}
}
}
} // I get a error here saying It expect route whereas it is Future[Route]
}
}
}
}
}
I get the above mentioned error, also I can not change the return type of validate () , is there a way to fix this issue. I need a way to return route instead of Future[Route]
If you've a handleRejections directive registered, you can use the onSuccess directive on Future:
onSuccess(validate()) {
case Right(_) => route
case Left(ex) => complete(StatusCodes.Unauthorized -> ex.getMessage)
}
Else you can use onComplete directive and you'll have to match Success and Failure
If you HAVE to return route instead of Future[Route], you can try using
Await.result(validate(), Duration(2, TimeUnit.SECONDS)) //substitute your choice of duration
this will block till validate returns.
So the full solution will look like,
// validateRoute act like a directive which validate a route before proceeding further
override def validateRoute(route: Route)(implicit ec: ExecutionContext): Route = {
extractRequest { request =>
decodeRequest {
entity(as[String]) { content =>
(headerValueByName("header1") & headerValueByName("header2")) {
case (header1, header2) => {
// dom some
// validate() returns a Future[Either[Error, Boolean]]
Await.result(validate(),Duration(2, TimeUnit.SECONDS) ) match
{
case Right(_) => route
case Left(ex) => complete(StatusCodes.Unauthorized -> ex.getMessage)
}
}
}
}
}
}
}
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.
}}
}
In my socket I receive object which I should add to my database if it doesn't exists, here is my code but the problem is that nothing is added to database (the method create works fine):
def receive = {
case result: Result =>
sessionService.findById(result.id).flatMap {
case Some(session) =>
Future.successful(out ! Json.toJson("exists"))
case None =>
val session = Session(
Option(java.util.UUID.randomUUID),
java.util.UUID.randomUUID,
java.util.UUID.randomUUID,
resultItem.versionId,
java.util.UUID.randomUUID,
"text",
"text",
null,
null,
Option(result.scores),
null)
sessionService.create(session).map {
case Right(_id) =>
println(Json.toJson(Json.obj("_id" -> _id)))
case Left(error) =>
println(Json.toJson("message" -> error))
}
}
}
UPDATE:
here is the create method:
def create(session: Session): Future[Either[String, UUID]] = {
println("here")
collection.insert(session).map {
case result if result.ok == true => Right(session._id.get)
case result => Left(result.writeErrors(0).errmsg)
}
}
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)))