how to implement validation in case class used as form (using Json.fromJson)

110 views Asked by At

i am using TypeSafe activator template as a basis for an app (play+reactivemongo+knockoutJS).

i am trying to create a user, and while doing it i want to validate that the country they provided is already in the DB. but due to the structure of the program, i am finding it hard to put in that validation.

code

Object UserContoller extends Controller {
  case class UserForm(
      firstName: String, 
      midName: Option[String], 
      lastName: String, 
      addr1: String,
      addr2: Option[String],
      addr3: Option[String],
      city: String,
      state: Option[String],
      zip: String, 
      country: String,
      email: String,
      phone: String
      ) {

    def toUser: User = User(
      BSONObjectID.generate, 
      Name(firstName, midName, lastName), 
      Vector(Email(email)), 
      Vector(Phone(phone)), 
      Address(addr1, addr2, addr3, city, state, zip, CountryDao.find(country))
    )
  }

  implicit val userFormFormat = Json.format[UserForm]

  def save = Action(parse.json) { req =>
    Json.fromJson[UserForm](req.body).fold(
      invalid => {
        BadRequest("Bad form").flashing(Flash(Map("errors" -> JsError(invalid).toString)))
      },
      form => Async {
        UserDao.save(form.toUser).map(_ => Created.flashing(Flash(Map("result" -> "success"))))
      }
    )
  }
}

my problem is to-fold: i need to verify the country exists in the DB, and i need to get the country for creating the User (which is an async action).

my best idea is to have UserForm implement something that provides a 'fold' method (as in Json...fold), so i can return invalid if the country is not found. if i know it exists, it's probably easier building the MongoDB query, awaiting it and doing a .get on the Option, as i already know it exists.

hope i made myself clear.

any ideas?

[edit] accepted suggestion below, with some changes: you gave me the direction, but i modified it a bit: i changed the UserForm class to take a Country as a parameter instead of a [string] country name. figured i would pass the list to the template which will render to a list, and the selected Country will be JSONed and uploaded as is (Country has a json.format).

i created exists(c: Country) method in CountryDao, which just does a find to the DB and returns the result.

then, i changed the saveUser as such:

...
      form => Async {
        CountryDao.exists(form.country) match {
          case c if c==true => {
            UserDao.save(form.toUser map {_ => 
              Created.flashing(Flash(Map("result" -> "success")))
            }          
          }
          case c if c==false => {
            Future(BadRequest("Invalid Country").flashing(
              Flash(Map("errors" -> "Invalid Country"))))
          }
        }
      }

and it compiles. let's see if it works too :)

anyway, i think that resolved it for now. thank you mantithetical

1

There are 1 answers

0
mantithetical On BEST ANSWER

One option is to do something like the following:

def save = Action(parse.json) { req =>
  Json.fromJson[UserForm](req.body).fold(
    invalid => {
      BadRequest("Bad form").flashing(Flash(Map("errors" -> JsError(invalid).toString)))
    },
    form => Async {
      CountryDao.find(form.country).map { c => // assuming find returns an Option
        UserDao.save(form.toUser(c) /* or form.toUser() */).map(
          _ => Created.flashing(Flash(Map("result" -> "success"))))
      }.getOrElse(
          BadRequest("Invalid Country").flashing(
            Flash(Map("errors" -> "Invalid Country")))) // or something else more appropriate
    }
  )
}

You could modify your toUser function to take an argument country of type Country if you didn't want to look up country twice.

def toUser (country: Country): User = User(
  BSONObjectID.generate, 
  Name(firstName, midName, lastName), 
  Vector(Email(email)), 
  Vector(Phone(phone)), 
  Address(addr1, addr2, addr3, city, state, zip, country)
)