Custom Directives

Part of spray-routings power comes from the ease with which it’s possible to define custom directives at differing levels of abstraction. There are essentially three ways of creating custom directives:

  1. By introducing new “labels” for configurations of existing directives
  2. By transforming existing directives
  3. By writing a directive “from scratch”

Configuration Labelling

The easiest way to create a custom directive is to simply assign a new name for a certain configuration of one or more existing directives. In fact, most of spray-routings predefined directives can be considered named configurations of more low-level directives.

The basic technique is explained in the chapter about Composing Directives, where, for example, a new directive getOrPut is defined like this:

val getOrPut = get | put

Another example are the MethodDirectives, which are simply instances of a preconfigured method directive, such as:

val delete = method(DELETE)
val get = method(GET)
val head = method(HEAD)
val options = method(OPTIONS)
val patch = method(PATCH)
val post = method(POST)
val put = method(PUT)

The low-level directives that most often form the basis of higher-level “named configuration” directives are grouped together in the BasicDirectives trait.

Transforming Directives

The second option for creating new directives is to transform an existing one using one of the “transformation methods”, which are defined on the Directive class, the base class of all “regular” directives.

Apart from the combinator operators (| and &) and the case-class extractor (as[T]) there are these transformations defined on all Directive[L <: HList] instances:

map / hmap

The hmap modifier has this signature (somewhat simplified):

def hmap[R](f: L => R): Directive[R :: HNil]

It can be used to transform the HList of extractions into another HList. The number and/or types of the extractions can be changed arbitrarily. If R <: HList then the result is Directive[R]. Here is a somewhat contrived example:

import shapeless._
import spray.routing._
import Directives._

val twoIntParameters: Directive[Int :: Int :: HNil] =
  parameters('a.as[Int], 'b.as[Int])

val myDirective: Directive1[String] =
  twoIntParameters.hmap {
    case a :: b :: HNil => (a + b).toString
  }

// test `myDirective` using the testkit DSL
Get("/?a=2&b=5") ~> myDirective(x => complete(x)) ~> check {
  responseAs[String] === "7"
}

If the Directive is a single-value Directive, i.e. one that extracts exactly one value, you can also use the simple map modifier, which doesn’t take the directives HList as parameter but rather the single value itself.

One example of a predefined directive relying on map is the optionalHeaderValue directive.

flatMap / hflatMap

With hmap or map you can transform the values a directive extracts, but you cannot change the “extracting” nature of the directive. For example, if you have a directive extracting an Int you can use map to turn it into a directive that extracts that Int and doubles it, but you cannot transform it into a directive, that doubles all positive Int values and rejects all others.

In order to do the latter you need hflatMap or flatMap. The hflatMap modifier has this signature:

def hflatMap[R <: HList](f: L => Directive[R]): Directive[R]

The given function produces a new directive depending on the HList of extractions of the underlying one. As in the case of map / hmap there is also a single-value variant called flatMap, which simplifies the operation for Directives only extracting one single value.

Here is the (contrived) example from above, which doubles positive Int values and rejects all others:

import shapeless._
import spray.routing._
import Directives._

val intParameter: Directive1[Int] = parameter('a.as[Int])

val myDirective: Directive1[Int] =
  intParameter.flatMap {
    case a if a > 0 => provide(2 * a)
    case _ => reject
  }

// test `myDirective` using the testkit DSL
Get("/?a=21") ~> myDirective(i => complete(i.toString)) ~> check {
  responseAs[String] === "42"
}
Get("/?a=-18") ~> myDirective(i => complete(i.toString)) ~> check {
  handled must beFalse
}

A common pattern that relies on flatMap is to first extract a value from the RequestContext with the extract directive and then flatMap with some kind of filtering logic. For example, this is the implementation of the method directive:

/**
 * Rejects all requests whose HTTP method does not match the given one.
 */
def method(httpMethod: HttpMethod): Directive0 =
  extract(_.request.method).flatMap[HNil] {
    case `httpMethod`  pass
    case _             reject(MethodRejection(httpMethod))
  } & cancelAllRejections(ofType[MethodRejection])

The explicit type parameter [HNil] on the flatMap is needed in this case because the result of the flatMap is directly concatenated with the cancelAllRejections directive, thereby preventing “outside-in” inference of the type parameter value.

require / hrequire

The require modifier transforms a single-extraction directive into a directive without extractions, which filters the requests according the a predicate function. All requests, for which the predicate is false are rejected, all others pass unchanged.

The signature of require is this (slightly simplified):

def require[T](predicate: T => Boolean): Directive[HNil]

One example of a predefined directive relying on require is the first overload of the host directive.

You can only call require on single-extraction directives. The hrequire modifier is the more general variant, which takes a predicate of type HList => Boolean. It can therefore also be used on directives with several extractions.

recover / recoverPF

The recover modifier allows you “catch” rejections produced by the underlying directive and, instead of rejecting, produce an alternative directive with the same type(s) of extractions.

The signature of recover is this:

def recover(recovery: List[Rejection] => Directive[L]): Directive[L]

In many cases the very similar recoverPF modifier might be little bit easier to use since it doesn’t require the handling of all rejections:

def recoverPF(recovery: PartialFunction[List[Rejection], Directive[L]]): Directive[L]

One example of a predefined directive relying recoverPF is the optionalHeaderValue directive.

Directives from Scratch

The third option for creating custom directives is to do it “from scratch”, by directly subclassing the Directive class. The Directive is defined like this (leaving away operators and modifiers):

abstract class Directive[L <: HList] {
  def happly(f: L => Route): Route
}

It only has one abstract member that you need to implement, the happly method, which creates the Route the directives presents to the outside from its inner Route building function (taking the extractions as parameter).

Extractions are kept as a shapeless HList. Here are a few examples:

  • A Directive[HNil] extracts nothing (like the get directive). Because this type is used quite frequently spray-routing defines a type alias for it:

    type Directive0 = Directive[HNil]
    
  • A Directive[String :: HNil] extracts one String value (like the hostName directive). The type alias for it is:

    type Directive1[T] = Directive[T :: HNil]
    
  • A Directive[Int :: String :: HNil] extracts an Int value and a String value (like a parameters('a.as[Int], 'b.as[String] directive).

Keeping extractions as HLists has a lot of advantages, mainly great flexibility while upholding full type safety and “inferability”. However, the number of times where you’ll really have to fall back to defining a directive from scratch should be very small. In fact, if you find yourself in a position where a “from scratch” directive is your only option, we’d like to hear about it, so we can provide a higher-level “something” for other users.