Spray Directives: Creating Your Own, Simple Directive
May 24 2014The spray-routing package provides an excellent dsl for creating restful api’s with Scala and Akka. This dsl is powered by directives, small building blocks you compose to filter, process and compose requests and responses for your API. Building your own directives lets you create reusable components for your application and better organize your application.
I recently refactored some code in a Spray API to leverage custom directives. The Spray documentation provides a good reference on custom directives but I found myself getting hung up in a few places.
As an example we’re going to write a custom directive which produces a UUID for each request. Here’s how I want to use this custom directive:
generateUUID { uuid => path("foo") { get { //log the uuid, pass it to your app, or maybe just return it complete { uuid.toString } } } }
Usually you leverage existing directives to build custom directives. I (incorrectly) started with the provide
directive to provide a value to an inner route:
import spray.routing._ import java.util.UUID import Directives._ trait UuidDirectives { def generateUuid: Directive1[UUID] = { provide(UUID.randomUUID) } }
Before I explain what’s wrong, let’s dig into the code. First, generateUuid is a function which returns a Directive1 wrapping a UUID value. Directive1 is just a type alias for Directive[UUID :: HNil]
. Directives are centered around a feature of the shapeless library called heterogeneous lists, or HLists. An HList
is simply a list, but each element in the list can be a different, specific type. Instead of a generic List[Any]
, your list can be composed of specific types of list of String, Int, String, UUID. The first element of this list is a String, not an Any, and the second is an Int, with all the features of an Int. In the directive above I just have an HList
with one element: UUID
. If I write Directive[UUID :: String :: HNil]
I have a two element list of UUID
and String, and the compiler will throw an error if I try to use this directive with anything other a UUID
and a String. HLists sound like a lightweight case class, but with an HList
, you get a lot of list-like features. HLists allow the compiler to do the heavy lifting of type safety, so you can have strongly-typed functions to compose together.
Provide is a directive which (surprise surprise) will provide a value to an inner route. I thought this would be perfect for my directive, and the corresponding test ensures it works:
import org.scalatest._ import org.scalatest.matchers._ import spray.testkit.ScalatestRouteTest import spray.http._ import spray.routing.Directives._ class UuidDirectivesSpec extends FreeSpec with Matchers with UuidDirectives with ScalatestRouteTest { "The UUID Directive" - { "can generate a UUID" in { Get() ~> generateUuid { uuid => complete(uuid.toString) } ~> check { responseAs[String].size shouldBe 36 } } } }
But there’s an issue! Spray directives are classes are composed when instantiated via an apply() function. The Spray docs on understanding the dsl structure explains it best, but in summary, generateUuid will only be called once when the routing tree is built, not on every request.
A better unit test shows the issue:
"will generate different UUID per request" in { //like the runtime, instantiate route once val uuidRoute = generateUuid { uuid => complete(uuid.toString) } var uuid1: String = "" var uuid2: String = "" Get() ~> uuidRoute ~> check { responseAs[String].size shouldBe 36 uuid1 = responseAs[String] } Get() ~> uuidRoute ~> check { responseAs[String].size shouldBe 36 uuid2 = responseAs[String] } //fails! uuid1 shouldNot equal (uuid2) } }
The fix is simple: we need to use the extract
directive which applies the current RequestContext to our route so it’s called on every request. For our UUID directive we don’t need anything from the request, just the function which is run for every request:
trait UuidDirectives { def generateUuid: Directive[UUID :: HNil] = { extract(ctx => UUID.randomUUID) } }
With our randomUUID call wrapped in an extract directive we have a unique call per request, and our tests pass!
In a following post we’ll add some more complexity to our custom directive, stay tuned!