diff --git a/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/CustomDirectivesExamplesSpec.scala b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/CustomDirectivesExamplesSpec.scala new file mode 100644 index 0000000000..c77709484c --- /dev/null +++ b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/CustomDirectivesExamplesSpec.scala @@ -0,0 +1,73 @@ +/** + * Copyright (C) 2009-2015 Typesafe Inc. + */ +package docs.http.scaladsl.server +package directives + +import akka.http.scaladsl.server.{ Directive1, Directive } +import docs.http.scaladsl.server.RoutingSpec + +class CustomDirectivesExamplesSpec extends RoutingSpec { + + "labeling" in { + val getOrPut = get | put + + // tests: + val route = getOrPut { complete("ok") } + + Get("/") ~> route ~> check { + responseAs[String] shouldEqual "ok" + } + + Put("/") ~> route ~> check { + responseAs[String] shouldEqual "ok" + } + } + + "map-0" in { + val textParam: Directive1[String] = + parameter("text".as[String]) + + val lengthDirective: Directive1[Int] = + textParam.map(text => text.length) + + // tests: + Get("/?text=abcdefg") ~> lengthDirective(x => complete(x.toString)) ~> check { + responseAs[String] === "7" + } + } + + "tmap-1" in { + val twoIntParameters: Directive[(Int, Int)] = + parameters(("a".as[Int], "b".as[Int])) + + val myDirective: Directive1[String] = + twoIntParameters.tmap { + case (a, b) => (a + b).toString + } + + // tests: + Get("/?a=2&b=5") ~> myDirective(x => complete(x)) ~> check { + responseAs[String] === "7" + } + } + + "flatMap-0" in { + val intParameter: Directive1[Int] = parameter("a".as[Int]) + + val myDirective: Directive1[Int] = + intParameter.flatMap { + case a if a > 0 => provide(2 * a) + case _ => reject + } + + // tests: + Get("/?a=21") ~> myDirective(i => complete(i.toString)) ~> check { + responseAs[String] === "42" + } + Get("/?a=-18") ~> myDirective(i => complete(i.toString)) ~> check { + handled === false + } + } + +} diff --git a/akka-docs-dev/rst/scala/http/routing-dsl/directives/custom-directives.rst b/akka-docs-dev/rst/scala/http/routing-dsl/directives/custom-directives.rst index b7f33f9208..e45901cb58 100644 --- a/akka-docs-dev/rst/scala/http/routing-dsl/directives/custom-directives.rst +++ b/akka-docs-dev/rst/scala/http/routing-dsl/directives/custom-directives.rst @@ -3,4 +3,194 @@ Custom Directives ================= -... +Part of the power of akka-http directives 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 Labeling +______________________ +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 the predefined akka-http 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: + +.. includecode2:: ../../../code/docs/http/scaladsl/server/directives/CustomDirectivesExamplesSpec.scala + :snippet: labeling + +Another example is the :ref:`MethodDirectives` which are simply instances of a preconfigured :ref:`-method-` directive. +The low-level directives that most often form the basis of higher-level “named configuration” directives are grouped +together in the :ref:`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. + +__ @github@/akka-http/src/main/scala/akka/http/scaladsl/server/Directive.scala + +Apart from the combinator operators (``|`` and ``&``) and the case-class extractor (``as[T]``) +there following transformations is also defined on all ``Directive`` instances: + + * :ref:`map/tmap` + * :ref:`flatMap/tflatMap` + * :ref:`require/trequire` + * :ref:`recover/recoverPF` + +.. _map/tmap: + +map and tmap +------------ +If the Directive is a single-value ``Directive``, the ``map`` method allows +for simple transformations: + +.. includecode2:: ../../../code/docs/http/scaladsl/server/directives/CustomDirectivesExamplesSpec.scala + :snippet: map-0 + +One example of a predefined directive relying on ``map`` is the `optionalHeaderValue`__ directive. + +__ @github@/akka-http/src/main/scala/akka/http/scaladsl/server/directives/HeaderDirectives.scala#L67 + +The tmap modifier has this signature (somewhat simplified):: + + def tmap[R](f: L ⇒ R): Directive[Out] + +It can be used to transform the ``Tuple`` of extractions into another ``Tuple``. +The number and/or types of the extractions can be changed arbitrarily. For example +if ``R`` is ``Tuple2[A, B]`` then the result will be a ``Directive[(A, B)]``. Here is a +somewhat contrived example: + +.. includecode2:: ../../../code/docs/http/scaladsl/server/directives/CustomDirectivesExamplesSpec.scala + :snippet: tmap-1 + + + +.. _flatMap/tflatMap: + +flatMap and tflatMap +-------------------- + +With map and tmap 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 ``flatMap`` or ``tflatMap``. The ``tflatMap`` +modifier has this signature:: + + def tflatMap[R: Tuple](f: L ⇒ Directive[R]): Directive[R] + +The given function produces a new directive depending on the Tuple of extractions +of the underlying one. As in the case of :ref:`map/tmap` 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: + +.. includecode2:: ../../../code/docs/http/scaladsl/server/directives/CustomDirectivesExamplesSpec.scala + :snippet: flatMap-0 + +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: + +.. includecode2:: ../../../../../../akka-http/src/main/scala/akka/http/scaladsl/server/directives/MethodDirectives.scala + :snippet: method + +The explicit type parameter ``[Unit]`` on the flatMap i`s 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/trequire: + +require and trequire +-------------------- + +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:: + + def require(predicate: T ⇒ Boolean, rejections: Rejection*): Directive0 + +One example of a predefined directive relying on require is the first overload of the host directive: + +.. includecode2:: ../../../../../../akka-http/src/main/scala/akka/http/scaladsl/server/directives/HostDirectives.scala + :snippet: require-host + +You can only call require on single-extraction directives. The trequire modifier is the +more general variant, which takes a predicate of type ``Tuple => Boolean``. +It can therefore also be used on directives with several extractions. + + +.. _recover/recoverPF: + +recover and 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[R >: L: Tuple](recovery: Seq[Rejection] ⇒ Directive[R]): Directive[R] = + +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[R >: L: Tuple]( + recovery: PartialFunction[Seq[Rejection], Directive[R]]): Directive[R] + + +One example of a predefined directive relying ``recoverPF`` is the optionalHeaderValue directive: + +.. includecode2:: ../../../../../../akka-http/src/main/scala/akka/http/scaladsl/server/directives/HeaderDirectives.scala + :snippet: optional-header + + + +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): + +.. includecode2:: ../../../../../../akka-http/src/main/scala/akka/http/scaladsl/server/Directive.scala + :snippet: basic + +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 Tuple. Here are a few examples: + +A ``Directive[Unit]`` extracts nothing (like the get directive). +Because this type is used quite frequently akka-http defines a type alias for it:: + + type Directive0 = Directive[Unit] + +A ``Directive[(String)]`` extracts one String value (like the hostName directive). The type alias for it is:: + + type Directive1[T] = Directive[Tuple1[T]] + +A Directive[(Int, String)] extracts an ``Int`` value and a ``String`` value +(like a ``parameters('a.as[Int], 'b.as[String])`` directive). + +Keeping extractions as ``Tuples`` 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. diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/Directive.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/Directive.scala index a2f0c0e34b..87e86c1e04 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/Directive.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/Directive.scala @@ -13,6 +13,7 @@ import akka.http.scaladsl.util.FastFuture._ /** * A directive that provides a tuple of values of type `L` to create an inner route. */ +//#basic abstract class Directive[L](implicit val ev: Tuple[L]) { /** @@ -22,7 +23,7 @@ abstract class Directive[L](implicit val ev: Tuple[L]) { * which is added by an implicit conversion (see `Directive.addDirectiveApply`). */ def tapply(f: L ⇒ Route): Route - +//# /** * Joins two directives into one which runs the second directive if the first one rejects. */ @@ -88,7 +89,10 @@ abstract class Directive[L](implicit val ev: Tuple[L]) { */ def recoverPF[R >: L: Tuple](recovery: PartialFunction[immutable.Seq[Rejection], Directive[R]]): Directive[R] = recover { rejections ⇒ recovery.applyOrElse(rejections, (rejs: Seq[Rejection]) ⇒ RouteDirectives.reject(rejs: _*)) } + +//#basic } +//# object Directive { diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/HeaderDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/HeaderDirectives.scala index 44c1ea20b8..35988246a5 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/HeaderDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/HeaderDirectives.scala @@ -59,6 +59,7 @@ trait HeaderDirectives { def headerValueByType[T <: HttpHeader](magnet: ClassMagnet[T]): Directive1[T] = headerValuePF(magnet.extractPF) | reject(MissingHeaderRejection(magnet.runtimeClass.getSimpleName)) + //#optional-header /** * Extracts an optional HTTP header value using the given function. * If the given function throws an exception the request is rejected @@ -68,6 +69,7 @@ trait HeaderDirectives { headerValue(f).map(Some(_): Option[T]).recoverPF { case Nil ⇒ provide(None) } + //# /** * Extracts an optional HTTP header value using the given partial function. diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/HostDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/HostDirectives.scala index 977978ba74..9d23e93e48 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/HostDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/HostDirectives.scala @@ -22,10 +22,12 @@ trait HostDirectives { */ def host(hostNames: String*): Directive0 = host(hostNames.contains(_)) + //#require-host /** * Rejects all requests for whose host name the given predicate function returns false. */ def host(predicate: String ⇒ Boolean): Directive0 = extractHost.require(predicate) + //# /** * Rejects all requests with a host name that doesn't have a prefix matching the given regular expression. diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MethodDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MethodDirectives.scala index 820cb0cd62..2e552f1dac 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MethodDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MethodDirectives.scala @@ -54,6 +54,7 @@ trait MethodDirectives { */ def extractMethod: Directive1[HttpMethod] = _extractMethod + //#method /** * Rejects all requests whose HTTP method does not match the given one. */ @@ -62,6 +63,7 @@ trait MethodDirectives { case `httpMethod` ⇒ pass case _ ⇒ reject(MethodRejection(httpMethod)) } & cancelRejections(classOf[MethodRejection]) + //# /** * Changes the HTTP method of the request to the value of the specified query string parameter. If the query string