SkyWalker
SkyWalker

Reputation: 14307

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.

Upvotes: 2

Views: 1245

Answers (1)

Astrid
Astrid

Reputation: 1828

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

Upvotes: 2

Related Questions