Case Class Extraction

Johannes Rudolph edited this page Aug 14, 2013 · 7 revisions

. . . Deprecation Note

This documentation is for release 0.9.0 (from 03/2012), which is built against Scala 2.9.1 and Akka 1.3.1 (see Requirements for more information). Most likely, this is not the place you want to look for information. Please turn to the main spray site at http://spray.io for more information about other available versions.

. . .

The value extraction performed by spray directives is a nice way of providing your application or route logic with interesting request properties, all with proper type-safety and error handling. However, in some case you might want even more. Consider this example:

case class Color(red: Int, green: Int, blue: Int)

val route =
  path("color") {
    parameters('red as[Int], 'green as[Int], 'blue as[Int]) { (red, green, blue) =>
      val color = Color(red, green, blue)
      ... // route working with the Color instance
    }
  }

Here you are employing a parameters directives to extract three Int values, which are used to construct an instance of the Color case class. So far so good. However, if the classes you'd like to work with have more than just a few parameters the overhead introduced by capturing the arguments as extractions only to feed them into the class constructor directly afterwards can somewhat clutter up your route definitions.

If your classes are case classes spray supports an even shorter and more concise syntax. You can also write the example above like this:

case class Color(red: Int, green: Int, blue: Int)

val route =
  path("color") {
    parameters('red as[Int], 'green as[Int], 'blue as[Int])
            .as(Color) { color =>
      ... // route working with the Color instance
    }
  }

You can postfix any directive with extractions with an as(...) call. The as method takes a Deserializer instance as argument, which is essentially a function Either[DeserializationError, T]. Usually you don't supply this deserializer directly but rather rely on implicit conversions that construct the proper deserializer from the apply method of your cases classes companion object. Most of the time it's enough to simply say as(CaseClassCompanionObject), which performs all the magic with the help of the Scala compiler (without any reflection involved and without any further requirements on your case class that is!). The only requirement is that the directive you attach the as call to produces the right number of extractions, with the right types and in the right order.

If you'd like to construct a case class instance from extractions produced by several directives you can first join the directives with the & operator before using the as call:

case class Color(name: String, red: Int, green: Int, blue: Int)

val route =
  (path("color" / PathElement) &
          parameters('red as[Int], 'green as[Int], 'blue as[Int]))
                  .as(Color) { color =>
    ... // route working with the Color instance
  }

Here the Color class has gotten another member, name, which is supplied not as a parameter but as a path element. By joining the path and parameters directives with & you create a directive extracting 4 values, which directly fit the member list of the Color case class. Therefore you can use the as call to directly create a Color instance from the extractions.

Generally, when you have routes that work with, say, more than 3 extractions it's a good idea to introduce a case class for these and use sprays case class extraction. Especially since it supports an nice feature: validation.

Validation of Case Class Instances

In many cases your web service needs to verify input parameters according to some logic before actually working with them. E.g. in the example above the restriction might be that all color component values must be between 0 and 255. You could get this done with a few validate directives but this would quickly become cumbersome and hard to read.

If you use case class extraction you can put the verification logic into the constructor of your case class, where it should be:

case class Color(name: String, red: Int, green: Int, blue: Int) {
  require(!name.isEmpty, "color name must not be empty")
  require(0 <= red && red <= 255, "red color component must be between 0 and 255")
  require(0 <= green && green <= 255, "green color component must be between 0 and 255")
  require(0 <= blue && blue <= 255, "blue color component must be between 0 and 255")
}

If you write your validations like this sprays case class extraction logic will properly pick up all error messages and generate a ValidationRejection if something goes wrong. By default, ValidationRejections trigger a corresponding 400 Bad Request error response, if no subsequent route successfully handles the request.

Quirk

There is one quirk to look out for when using case class extraction: If you create an explicit companion object for your case class, no matter whether you actually add any members to it or not, the syntax presented above will not (quite) work anymore. Instead of as(Color) you will then have to say as(Color.apply). This behavior appears as if it's not really intended, we will try to work with the guys at TypeSafe to fix this.