Directives
Loading

Directives

A "Directive" is a small building block to construct arbitrarily complex route structures. Here is a simple example of a route built from directives:

val route: Route =
  path("order" / IntNumber) { id =>
    get {
      complete {
        "Received GET request for order " + id
      }
    } ~
      put {
        complete {
          "Received PUT request for order " + id
        }
      }
  }

The general anatomy of a directive is as follows:

name(arguments) { extractions =>
  ... // inner Route
}

It has a name, zero or more arguments and optionally an inner Route. Additionally directives can "extract" a number of values and make them available to their inner routes as function arguments. When seen "from the outside" a directive with its inner Route form an expression of type Route.

What Directives do

A directive can do one or more of the following:

  • Transform the incoming RequestContext before passing it on to its inner Route
  • Filter the RequestContext according to some logic, i.e. only pass on certain requests and reject all others
  • Extract values from the RequestContext and make them available to its inner Route as "extractions"
  • Complete the request

The first point deserves some more discussion. The RequestContext is the central object that is passed on through a route structure (also see RequestContext). When a directive (or better the Route it built) receives a RequestContext it can decide to pass this instance on unchanged to its inner Route or it can create a copy of the RequestContext instance, with one or more changes, and pass on this copy to its inner Route. Typically this is good for two things:

  • Transforming the HttpRequest instance
  • "Hooking in" a response transformation function that changes the RouteResponse (and therefore the response).

This means a Directive completely wraps the functionality of its inner routes and can apply arbitrarily complex transformations, both (or either) on the request and on the response side.

Composing Directives

As you have seen from the examples presented so far the "normal" way of composing directives is nesting. Let's take another look at the example from above:

val route: Route =
  path("order" / IntNumber) { id =>
    get {
      complete {
        "Received GET request for order " + id
      }
    } ~
      put {
        complete {
          "Received PUT request for order " + id
        }
      }
  }

Here the get and put directives are chained together with the ~ operator to form a higher-level route that serves as the inner Route of the path directive. To make this structure more explicit you could also write the whole thing like this:

def innerRoute(id: Int): Route =
  get {
    complete {
      "Received GET request for order " + id
    }
  } ~
    put {
      complete {
        "Received PUT request for order " + id
      }
    }

val route: Route = path("order" / IntNumber) { id => innerRoute(id) }

What you can't see from this snippet is that directives are not implemented as simple methods but rather as stand-alone objects of type Directive. This gives you more flexibility when composing directives. For example you can also use the | operator on directives. Here is yet another way to write the example:

val route =
  path("order" / IntNumber) { id =>
    (get | put) { ctx =>
      ctx.complete("Received " + ctx.request.method.name + " request for order " + id)
    }
  }

If you have a larger route structure where the (get | put) snippet appears several times you could also factor it out like this:

val getOrPut = get | put
val route =
  path("order" / IntNumber) { id =>
    getOrPut { ctx =>
      ctx.complete("Received " + ctx.request.method.name + " request for order " + id)
    }
  }

As an alternative to nesting you can also use the & operator:

val getOrPut = get | put
val route =
  (path("order" / IntNumber) & getOrPut) { id =>
    ctx =>
      ctx.complete("Received " + ctx.request.method.name + " request for order " + id)
  }

And once again, you can factor things out if you want:

val orderGetOrPut = path("order" / IntNumber) & (get | put)
val route =
  orderGetOrPut { id =>
    ctx =>
      ctx.complete("Received " + ctx.request.method.name + " request for order " + id)
  }

This type of combining directives with the | and & operators as well as "saving" more complex directive configurations as a val works across the board, with all directives taking inner routes.

There is one more "ugly" thing remaining in our snippet: we have to fall back to the lowest-level route definition, directly manipulating the RequestContext, in order to get to the request method. It'd be nicer if we could somehow "extract" the method name in a special directive, so that we can express our inner-most route with a simple complete. As it turns out this is easy with the extract directive:

val orderGetOrPut = path("order" / IntNumber) & (get | put)
val requestMethod = extract(_.request.method)
val route =
  orderGetOrPut { id =>
    requestMethod { m =>
      complete("Received " + m.name + " request for order " + id)
    }
  }

Or differently:

val orderGetOrPut = path("order" / IntNumber) & (get | put)
val requestMethod = extract(_.request.method)
val route =
  (orderGetOrPut & requestMethod) { (id, m) =>
    complete("Received " + m.name + " request for order " + id)
  }

Now, pushing the "factoring out" of directive configurations to its extreme, we end up with this:

val orderGetOrPutMethod =
  path("order" / IntNumber) & (get | put) & extract(_.request.method)
val route =
  orderGetOrPutMethod { (id, m) =>
    complete("Received " + m.name + " request for order " + id)
  }

Note that going this far with "compressing" several directives into a single one probably doesn't result in the most readable and therefore maintainable routing code. It might even be that the very first of this series of examples is in fact the most readable one.

Still, the purpose of the exercise presented here is to show you how flexible directives can be and how you can use their power to define your web service behavior at the level of abstraction that is right for your application.

Type Safety of Directives

When you combine directives with the | and & operators the routing DSL makes sure that all extractions work as expected and logical constraints are enforced at compile-time.

For example you cannot | a directive producing an extraction with one that doesn't:

val route = path("order" / IntNumber) | get // doesn't compile

Also the number of extractions and their types have to match up:

val route = path("order" / IntNumber) | path("order" / DoubleNumber)   // doesn't compile
val route = path("order" / IntNumber) | parameter('order.as[Int])      // ok

When you combine directives producing extractions with the & operator all extractions will be properly gathered up:

val order = path("order" / IntNumber) & parameters('oem, 'expired ?)
val route =
  order { (orderId, oem, expired) =>
    ...
  }

Directives offer a great way of constructing your web service logic from small building blocks in a plug and play fashion while maintaining DRYness and full type-safety. If the large range of Predefined Directives (alphabetically) does not fully satisfy your needs you can also very easily create Custom Directives.

Contents