Directives
“Directives” are small building blocks of which you can construct arbitrarily complex route structures. Here is a simple example of a route built from directives:
import spray.routing._
import 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
(see the Routes chapter for more details).
What Directives do
A directive does 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. A RequestContext
is the central object that is passed on through a
route structure and, potentially, in between actors. It’s immutable but light-weight and can therefore be copied
quickly. When a directive receives a RequestContext
instance from the outside 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” another response transformation function into the responder chain.
The Responder Chain
For understanding the “responder chain” it is helpful to look at what happens when the complete
method of a
RequestContext
instance is called in the inner-most route of a route structure.
Consider the following hypothetical route structure of three nested directives around a simple route:
foo {
bar {
baz {
ctx => ctx.complete("Hello")
}
}
}
Assume that foo and baz “hook in” response transformation logic whereas bar leaves the responder
of the
RequestContext
it receives unchanged before passing it on to its inner Route. This is what happens when the
complete("Hello")
is called:
- The
complete
method creates anHttpResponse
an sends it to responder of theRequestContext
. - The response transformation logic supplied by the baz directive runs and sends its result to the responder
of the
RequestContext
the baz directive received. - The response transformation logic supplied by the foo directive runs and sends its result to the responder
of the
RequestContext
the foo directive received. - The responder of the original
RequestContext
, which is the senderActorRef
of theHttpRequest
, receives the response and sends it out to the client.
As you can see all response handling logic forms a logic chain that directives can choose to “hook into”.
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 + " 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 + " 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 + " 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 + " 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 + " 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 + " 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 + " 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
When you combine directives with the |
and &
operators spray-routing 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.