Binding classes and enumerations in Play! routes and query parameters

Back

Easy Custom class/enum Route & Query binding in Play! Framework

TL;DR;

Both PathBindable and QueryStringBindable have an inner class called Parsing which can be used to quickly define a binding as you see fit. Use this to write your methods without having to define a *Bindable with two overridden methods yourself.

From what I can tell you can't bind a class from a route parameter unless it's possible to create your class from a single parameter. This is because path binders only have 1 value available for the binder at a time.

Quick Examples

Note: See the details section below for some important caveats about these examples

Bind a class in the route:

case class TestObject(a: String, b: Int)

object TestObject {
    /** Define String => T, T => String, (String, Exception) => String 
     *  to handle binding, unbinding, and exception handling
     */
    implicit object testObjectRouteBinder extends PathBindable.Parsing[TestObject](
        TestObject(_, 0/*<-- this is set by you because the route cant do both a & b*/), 
        _.a, 
        (key: String, e: Exception) => "Cannot parse %s as TestObject: %s".format(key, e.getMessage())
    )
}

class TestController() extends Controller {
    def testClass(param: TestObject) = Action {
        Ok(param.toString)
    }
}

// In route file:
GET /test/class/:name @controller.TestController.testClass(name: controller.TestObject)

Bind an Enumeration in the route:

object MyEnum extends Enumeration {
    type MyAlias = Value
    val foo = Value("foo")
    val bar = Value("bar")

    implicit object myEnumBinder extends PathBindable.Parsing[MyAlias](
        withName(_), _.toString, (k: String, e: Exception) => "Cannot parse %s as MyEnum: %s".format(k, e.getMessage())
    )
}

class TestController() extends Controller {
    def testEnum(enum: MyEnum.MyAlias) = Action {
        Ok(enum.toString)
    }
}

// In route file:
GET /test/enum/:value @controller.TestController.testEnum(value: controller.MyEnum.MyAlias)

Bind a class or enumeration from query parameters can be done in the same way as path binding but instead of PathBindable.Parsing you use QueryStringBindable.Parsing. However, if you need to use more than one value when setting up the binding, you'll need to do a little more work by overriding the bind and unbind methods like the documentation shows in the scaladoc. Note that if your case class has a string value, bring the implicit string binder from play into scope like so:

implicit def queryStringBinder(
    implicit intBinder: QueryStringBindable[Int], 
    stringBinder: QueryStringBindable[String]
) = new QueryStringBindable[TestObject] {
    override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, TestObject]] = {
        for {
            a <- stringBinder.bind(key + ".a", params)
            b <- intBinder.bind(key + ".b", params)
        } yield {
            (a, b) match {
                case (Right(a), Right(b)) => Right(TestObject(a, b))
                case _ => Left("Unable to bind a TestObject")
            }
        }
    }
    override def unbind(key: String, pager: TestObject): String = {
        stringBinder.unbind(key + ".a", pager.a) + "&" + intBinder.unbind(key + ".b", pager.b)
    }
}

you'll want to use the stringBinder here in the unbind method to properly escape the value. You don't want to generate an invalid query string!

There's no rule that says you need to use the key + .field when binding parameters. For example, if you knew that some class would always be bound with the same query parameter names you could hard code them and drop the . convention. The above binder would be used say if you had a route like

GET /test/q/class @controller.TestController.testClass(thing: controller.TestObject)

And would be called correctly with a request like

/test/q/class?thing.a=Test%20Thing&thing.b=3

Take note that the the key being bound here is pulled from thing defined as the argument name in the routes file.

Useful note about the above path examples

Because under our path binders convert to String and back, the default binder that play's framework defines is called on our values. That means that we don't have to worry about encoding or decoding the values from the URL's path ourselves.

A slightly more realistic example

So you're writing a controller in play, and you decide that for some parameter of your query, say a sorting or ordering parameter, you want to use an Enumeration like this:

object RequestedOrder extends Enumeration {
    type Order = Value

    val ascending = Value("ASC")
    val descending = Value("DSC")
}

And to go along with this you have a number of different field's you'll be sorting by, which you've also enumerated:

object SortingBy extends Enumeration {
    type Field = Value

    val id = Value("id")
    val someField = Value("someField")
    // etc ...
}

Your system has more than one data type though, and each has slightly different fields, so you define a few more enumerations (maybe you'd define them all in one enumeration, but for the sake of this example let's roll with this)

object DataType1SortableFields extends Enumeration {
    type Field = Value
    val id = Value("id")
    val someOtherField = Value("someOtherField")
    val anotherField = Value("anotherField")
}

This goes on for a while, and then you decide that because you're a good person (or because you're getting tired of writing error handling code in the controller for transforming raw Strings to your Enumeration types) you'll use the enumeration type in your routes/controller signatures. This is all well and dandy so you then define query/route binders for each of your enumerations:

implicit object testObjectRouteBinder extends PathBindable.Parsing[RequestedOrder](
    RequestedOrder.withName(_), _.String, (k: String, e: Exception) => "Cannot parse %s as RequestedOrder: %s".format(k, e.getMessage())
)
implicit object testObjectQueryBinder extends QueryStringBindable.Parsing[RequestedOrder](
    RequestedOrder.withName(_), _.String, (k: String, e: Exception) => "Cannot parse %s as RequestedOrder: %s".format(k, e.getMessage())
)
... repeat for SortingBy and DataType1SortableFields

Seems like we've still got a lot of boiler plate doesn't it? For each of our Enumeration's we need to define a parser for both the path and the query, and for each of these we need to pass in 3 functions for each with only minor variations to each. Luckily, we can write a factory method to make this a little less tedius:

object EnumPathAndRouteBinder {
    def binders[E <: Enumeration](enum: E): (PathBindable[E#Value], QueryStringBindable[E#Value]) = {
        /** Because I don't want to repeat a list of arguments */
        val args: (String => E#Value, E#Value => String, (String, Exception) => String) = (
            enum.withName(_),
            (e: E#Value) => e.toString,
            (k: String, e: Exception) => "Cannot parse %s as %s: %s".format(k, enum.getClass.getName(), e.getMessage())
        )
        ((new PathBindable.Parsing[E#Value](_, _, _)).tupled(args), (new QueryStringBindable.Parsing[E#Value](_, _, _)).tupled(args))
    }
}

And then call it like so outside of the enum objects:

// you'd probably give these better names in production code
implicit val (pathBinders1, queryBinders1) = EnumPathAndRouteBinder.binders(RequestedOrder)
implicit val (pathBinders2, queryBinders2) = EnumPathAndRouteBinder.binders(SortingBy)
implicit val (pathBinders3, queryBinders3) = EnumPathAndRouteBinder.binders(DataType1SortableFields)

Or you can call it within the objects extending Enumeration and then just pass this to it:

object DataType1SortableFields extends Enumeration {
    type Field = Value
    val id = Value("id")
    val someOtherField = Value("someOtherField")
    val anotherField = Value("anotherField")

    implicit val (pathBinders, queryBinders) = EnumPathAndRouteBinder.binders(this)
}

Let me explain the enum binder factory a little bit: As you're probably aware if you read my last post, nested types can't be made without a reference to their parent class or object. So that's why we have to pass in the object extending Enumeration to our binders method. Once we have it though, we can happily instantiate members of the inner class (whose type is E#Value) with withName. The odd looking:

((new PathBindable.Parsing[E#Value](_, _, _)).tupled(args), (new QueryStringBindable.Parsing[E#Value](_, _, _)).tupled(args))

Is taking the constructor for the two Parsing classes, converting them into partial functions, then using Function.tupled to pass the args to the constructor. The reason I did this is because I didn't want to write the same boiler plate for the parsers over and over. If this is confusing to you, this is the same code written without the use of tupled:

def binders[E <: Enumeration](enum: E): (PathBindable[E#Value], QueryStringBindable[E#Value]) = {
    val parse: String => E#Value = enum.withName(_)
    val serialize = (e: E#Value) => e.toString
    val err = (k: String, e: Exception) => "Cannot parse %s as %s: %s".format(k, enum.getClass.getName(), e.getMessage())
    (new PathBindable.Parsing(parse, serialize, err), new QueryStringBindable.Parsing(parse, serialize, err))
}

And with that helper object, you can more easily create bindings for your enumerations! Hopefully this helps.

Other Posts

comments powered by Disqus