cjwebb.github.io

I write software, teach people, and investigate new technology

Using Play-Framework's PathBindable

| Comments

Using custom types in Play Framework’s routes file is a major win, and is not something obviously supported. Consider the routes file below:

1
2
GET /stuff/:id     @controllers.StuffController(id: String)
GET /things/:id    @controllers.ThingsController(id: java.util.UUID)

In the first route, we take the id parameter as a String. In the second, we take it as a java.util.UUID.

Advantages

In our example above, paths that do not contains UUIDs are not matched for the second route. We don’t have to deal with IDs that are not UUIDs.

At the start of a project, you may see lots of lines that say:

1
2
3
4
id match {
   case i if isUUID(i) => doStuff()
   case _ => BadRequest(id must be a UUID)
}

By not matching on the route, we can remove this code. A request either matches a route, and is passed to the controller, or it doesn’t, and the controller never knows about the request.

By allowing types, and not just strings, you can avoid stringly-typed controllers. Admittedly, UUIDly-typed is only a small step in the right direction, but still a significant improvement.

Disadvantages

You need to fully-qualify the types in the routes file, for example by using java.util.UUID everywhere. You cannot use imports in the routes file. Hopefully someone will find a solution to that at some point.

Implementation

There are two things that need doing before you can use custom types in the routes file. Firstly, you must implement a PathBindable and its bind and unbind methods. For a UUID, this is quite simple. The bind method returns an Either so that you can return the a message for why the route did not match.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package util

import java.util.UUID
import play.api.mvc.PathBindable

object Binders {
   implicit def uuidPathBinder = new PathBindable[UUID] {
      override def bind(key: String, value: String): Either[String, UUID] = {
         try {
            Right(UUID.fromString(value))
         } catch {
            case e: IllegalArgumentException => Left("Id must be a UUID")
         }
      }
      override def unbind(key: String, value: UUID): String = value.toString
   }
}

Secondly, you must make Play aware of this class, by changing your build file.

1
2
3
import play.PlayImport.PlayKeys._

routesImport += "utils.Binders._"

After those two steps, you can then use types in the routes file.

Comments