Rasputin Jones
Rasputin Jones

Reputation: 1437

Optional Json Body Parser

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?

Upvotes: 3

Views: 1501

Answers (1)

EECOLOR
EECOLOR

Reputation: 11244

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)))

Upvotes: 2

Related Questions