Search code examples
scalaplayframeworkplay-framework-2.7

Scala Play: Routes optional parameter with regex?


For one of my routes I have an optional parameter i.e. birthDate: Option[String] and can do this:

GET /rest/api/findSomeone/:firstName/:lastName controllers.PeopleController.findSomeone(firstName: String, lastName: String, birthDate: Option[String])

However, to be more strict with the birthDate optional parameter it would be helpful to specify a regex like this:

$birthDate<([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))>

But since this is an optional parameter I can't find a way to do that .. it this covered in Play 2.7.x? I'm faced with the dilemma of making the birthDate parameter non-optional or leaving it unchecked.

As a side note. I had been trying to integrate routes binding of Joda time e.g. org.joda.time.LocalDate by adding the following dependency https://github.com/tototoshi/play-joda-routes-binder "com.github.tototoshi" %% "play-joda-routes-binder" % "1.3.0" but it didn't work in my project as I get compilation errors after integrating it so I stashed that approach away for the time being.


Solution

  • For parsing a date, I wouldn't recommend using a regex based validator at all. Instead, you could - for instance - use a custom case class with a query string binder which will do a type-safe parsing of the incoming parameter:

    package models
    
    import java.time.LocalDate
    import java.time.format.{DateTimeFormatter, DateTimeParseException}
    
    import play.api.mvc.QueryStringBindable
    
    case class BirthDate(date: LocalDate)
    
    object BirthDate {
      private val dateFormatter: DateTimeFormatter = DateTimeFormatter.ISO_DATE // or whatever date format you're using
    
      implicit val queryStringBindable = new QueryStringBindable[BirthDate] {
        override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, BirthDate]] = {
          params.get(key).flatMap(_.headOption).map { value =>
            try {
              Right(BirthDate(LocalDate.parse(value, dateFormatter)))
            } catch {
              case _: DateTimeParseException => Left(s"$value cannot be parsed as a date!")
            }
          }
        }
    
        override def unbind(key: String, value: BirthDate): String = {
          s"$key=${value.date.format(dateFormatter)}"
        }
      }
    }
    

    Now if you change your routes config so birthDate is a parameter of type Option[BirthDate], you'll get the behaviour you want.

    If you're insistent on using regexes, you could use a regex-based parser in place of the date formatter and have BirthDate wrap a String instead of a LocalDate, but for the use case presented I really don't see what the advantage of that would be.

    EDIT: just for completeness, the regex-based variant:

    case class BirthDate(date: String)
    
    object BirthDate {
      private val regex = "([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))".r
    
      implicit val queryStringBindable = new QueryStringBindable[BirthDate] {
        override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, BirthDate]] = {
          params.get(key).flatMap(_.headOption).map { value =>
            regex.findFirstIn(value).map(BirthDate.apply).toRight(s"$value cannot be parsed as a date!")
          }
        }
    
        override def unbind(key: String, value: BirthDate): String = {
          s"$key=${value.date}"
        }
      }
    }