From d83a323549494a3710e0c9e21728da953e8ef676 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 16 Mar 2016 15:03:47 +0100 Subject: [PATCH] =htp #20052 headerValueByType now works with custom headers --- .../HeaderDirectivesExamplesSpec.scala | 1 + .../rst/scala/http/common/http-model.rst | 2 + .../header-directives/headerValueByType.rst | 7 +++ .../optionalHeaderValueByType.rst | 7 +++ .../http/scaladsl/model/headers/headers.scala | 1 + .../http/javadsl/HttpExtensionApiSpec.scala | 1 - .../server/ModeledCustomHeaderSpec.scala | 46 +++++++++++----- .../http/javadsl/server/HttpService.scala | 3 +- .../http/javadsl/server/values/Header.scala | 4 +- .../server/directives/HeaderDirectives.scala | 53 ++++++++++++++++++- 10 files changed, 106 insertions(+), 19 deletions(-) diff --git a/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/HeaderDirectivesExamplesSpec.scala b/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/HeaderDirectivesExamplesSpec.scala index 68af843d54..0284c627f8 100644 --- a/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/HeaderDirectivesExamplesSpec.scala +++ b/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/HeaderDirectivesExamplesSpec.scala @@ -7,6 +7,7 @@ package docs.http.scaladsl.server.directives import akka.http.scaladsl.model._ import akka.http.scaladsl.server.MissingHeaderRejection import akka.http.scaladsl.server.Route +import akka.http.scaladsl.server.util.ClassMagnet import docs.http.scaladsl.server.RoutingSpec import headers._ import StatusCodes._ diff --git a/akka-docs/rst/scala/http/common/http-model.rst b/akka-docs/rst/scala/http/common/http-model.rst index af13c5342c..410b16b0c0 100644 --- a/akka-docs/rst/scala/http/common/http-model.rst +++ b/akka-docs/rst/scala/http/common/http-model.rst @@ -294,6 +294,8 @@ Strict-Transport-Security __ @github@/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala#L422 +.. _custom-headers-scala: + Custom Headers -------------- diff --git a/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/headerValueByType.rst b/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/headerValueByType.rst index 96d560a214..2f4c65f224 100644 --- a/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/headerValueByType.rst +++ b/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/headerValueByType.rst @@ -25,6 +25,13 @@ the given type is found the request is rejected with a ``MissingHeaderRejection` If the header is expected to be missing in some cases or to customize handling when the header is missing use the :ref:`-optionalHeaderValueByType-` directive instead. +.. note:: + Custom headers will only be matched by this directive if they extend ``ModeledCustomHeader`` + and provide a companion extending ``ModeledCustomHeaderCompanion``, otherwise the routing + infrastructure does now know where to search for the needed companion and header name. + + To learn more about defining custom headers, read: :ref:`custom-headers-scala`. + Example ------- diff --git a/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/optionalHeaderValueByType.rst b/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/optionalHeaderValueByType.rst index a54fa51956..e77ce8db82 100644 --- a/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/optionalHeaderValueByType.rst +++ b/akka-docs/rst/scala/http/routing-dsl/directives/header-directives/optionalHeaderValueByType.rst @@ -22,6 +22,13 @@ Optionally extracts the value of the HTTP request header of the given type. The ``optionalHeaderValueByType`` directive is similar to the :ref:`-headerValueByType-` directive but always extracts an ``Option`` value instead of rejecting the request if no matching header could be found. +.. note:: + Custom headers will only be matched by this directive if they extend ``ModeledCustomHeader`` + and provide a companion extending ``ModeledCustomHeaderCompanion``, otherwise the routing + infrastructure does now know where to search for the needed companion and header name. + + To learn more about defining custom headers, read: :ref:`custom-headers-scala`. + Example ------- diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala index 1e8822d7d9..a5510aefce 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/headers/headers.scala @@ -87,6 +87,7 @@ abstract class ModeledCustomHeaderCompanion[H <: ModeledCustomHeader[H]] { case _ ⇒ None } + final implicit val implicitlyLocatableCompanion: ModeledCustomHeaderCompanion[H] = this } /** diff --git a/akka-http-core/src/test/scala/akka/http/javadsl/HttpExtensionApiSpec.scala b/akka-http-core/src/test/scala/akka/http/javadsl/HttpExtensionApiSpec.scala index c0be925dc4..b86308eca1 100644 --- a/akka-http-core/src/test/scala/akka/http/javadsl/HttpExtensionApiSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/javadsl/HttpExtensionApiSpec.scala @@ -380,7 +380,6 @@ class HttpExtensionApiSpec extends WordSpec with Matchers with BeforeAndAfterAll "create an outgoing connection (with 6 parameters)" in { val (host, port, binding) = runServer() - println("host = " + host) val flow = http.outgoingConnection( toHost(host, port), Optional.empty(), diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/ModeledCustomHeaderSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/ModeledCustomHeaderSpec.scala index 95195ccb2b..f0336e7124 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/ModeledCustomHeaderSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/ModeledCustomHeaderSpec.scala @@ -6,6 +6,7 @@ package akka.http.scaladsl.server import akka.http.scaladsl.model.{ HttpHeader, StatusCodes } import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.server.directives.HeaderMagnet import scala.concurrent.Future import scala.util.{ Success, Failure, Try } @@ -13,20 +14,26 @@ import scala.util.{ Success, Failure, Try } object ModeledCustomHeaderSpec { //#modeled-api-key-custom-header - object ApiTokenHeader extends ModeledCustomHeaderCompanion[ApiTokenHeader] { - def renderInRequests = false - def renderInResponses = false - override val name = "apiKey" - override def parse(value: String) = Try(new ApiTokenHeader(value)) - } final class ApiTokenHeader(token: String) extends ModeledCustomHeader[ApiTokenHeader] { def renderInRequests = false def renderInResponses = false override val companion = ApiTokenHeader override def value: String = token } + object ApiTokenHeader extends ModeledCustomHeaderCompanion[ApiTokenHeader] { + def renderInRequests = false + def renderInResponses = false + override val name = "apiKey" + override def parse(value: String) = Try(new ApiTokenHeader(value)) + } //#modeled-api-key-custom-header + final class DifferentHeader(token: String) extends ModeledCustomHeader[DifferentHeader] { + def renderInRequests = false + def renderInResponses = false + override val companion = DifferentHeader + override def value = token + } object DifferentHeader extends ModeledCustomHeaderCompanion[DifferentHeader] { def renderInRequests = false def renderInResponses = false @@ -35,12 +42,6 @@ object ModeledCustomHeaderSpec { if (value contains " ") Failure(new Exception("Contains illegal whitespace!")) else Success(new DifferentHeader(value)) } - final class DifferentHeader(token: String) extends ModeledCustomHeader[DifferentHeader] { - def renderInRequests = false - def renderInResponses = false - override val companion = DifferentHeader - override def value = token - } } @@ -108,6 +109,27 @@ class ModeledCustomHeaderSpec extends RoutingSpec { //#matching-in-routes } + "be able to extract in routing DSL via headerValueByType" in { + val routes = headerValueByType[ApiTokenHeader]() { token ⇒ + val ApiTokenHeader(t) = token + complete(s"extracted> $t") + } + + Get().withHeaders(RawHeader("apiKey", "TheKey")) ~> routes ~> check { + status should ===(StatusCodes.OK) + responseAs[String] should ===("extracted> apiKey: TheKey") + } + + Get().withHeaders(RawHeader("somethingElse", "TheKey")) ~> routes ~> check { + rejection should ===(MissingHeaderRejection("ApiTokenHeader")) + } + + Get().withHeaders(ApiTokenHeader("TheKey")) ~> routes ~> check { + status should ===(StatusCodes.OK) + responseAs[String] should ===("extracted> apiKey: TheKey") + } + } + "fail with useful message when unable to parse" in { val ex = intercept[Exception] { DifferentHeader("Hello world") // illegal " " diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/HttpService.scala b/akka-http/src/main/scala/akka/http/javadsl/server/HttpService.scala index cc40c9dd90..13bfc167b2 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/HttpService.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/HttpService.scala @@ -7,7 +7,6 @@ package akka.http.javadsl.server import akka.actor.ActorSystem import akka.http.scaladsl.{ server, Http } import akka.http.scaladsl.Http.ServerBinding -import akka.http.scaladsl.server.RouteResult import akka.http.impl.server.RouteImplementation import akka.stream.{ ActorMaterializer, Materializer } import akka.stream.scaladsl.{ Keep, Sink } @@ -39,7 +38,7 @@ trait HttpServiceBase { import system.dispatcher val r: server.Route = RouteImplementation(route) - Http(system).bind(interface, port).toMat(Sink.foreach(_.handleWith(RouteResult.route2HandlerFlow(r))))(Keep.left).run()(materializer).toJava + Http(system).bind(interface, port).toMat(Sink.foreach(_.handleWith(akka.http.scaladsl.server.RouteResult.route2HandlerFlow(r))))(Keep.left).run()(materializer).toJava } } diff --git a/akka-http/src/main/scala/akka/http/javadsl/server/values/Header.scala b/akka-http/src/main/scala/akka/http/javadsl/server/values/Header.scala index 9e99770001..b0ca5bb3ab 100644 --- a/akka-http/src/main/scala/akka/http/javadsl/server/values/Header.scala +++ b/akka-http/src/main/scala/akka/http/javadsl/server/values/Header.scala @@ -11,7 +11,7 @@ import akka.http.javadsl.model.HttpHeader import akka.http.javadsl.server.RequestVal import akka.http.scaladsl.model import akka.http.scaladsl.server.Directive1 -import akka.http.scaladsl.server.util.ClassMagnet +import akka.http.scaladsl.server.directives.HeaderMagnet import scala.compat.java8.OptionConverters._ import scala.reflect.{ ClassTag, classTag } @@ -31,7 +31,7 @@ object Headers { HeaderImpl[HttpHeader](name, _ ⇒ optionalHeaderInstanceByName(name.toLowerCase()).map(_.asScala), classTag[HttpHeader]) def byClass[T <: HttpHeader](clazz: Class[T]): Header[T] = - HeaderImpl[T](clazz.getSimpleName, ct ⇒ optionalHeaderValueByType(ClassMagnet(ct)), ClassTag(clazz)) + HeaderImpl[T](clazz.getSimpleName, ct ⇒ optionalHeaderValueByType(HeaderMagnet.fromUnit(())(ct)), ClassTag(clazz)) private def optionalHeaderInstanceByName(lowercaseName: String): Directive1[Optional[model.HttpHeader]] = extract(_.request.headers.collectFirst { 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 6f0430dd71..ba3e7bf53f 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 @@ -5,7 +5,13 @@ package akka.http.scaladsl.server package directives +import akka.http.javadsl.model.headers.CustomHeader +import akka.http.scaladsl.model.headers.{ModeledCustomHeaderCompanion, ModeledCustomHeader, RawHeader} + +import scala.annotation.implicitNotFound +import scala.reflect.ClassTag import scala.util.control.NonFatal +import akka.http.javadsl.{ model => jm } import akka.http.scaladsl.server.util.ClassMagnet import akka.http.scaladsl.model._ import akka.http.impl.util._ @@ -55,8 +61,11 @@ trait HeaderDirectives { /** * Extracts the first HTTP request header of the given type. * If no header with a matching type is found the request is rejected with a [[akka.http.scaladsl.server.MissingHeaderRejection]]. + * + * Custom headers will only be matched by this directive if they extend [[ModeledCustomHeader]] + * and provide a companion extending [[ModeledCustomHeaderCompanion]]. */ - def headerValueByType[T <: HttpHeader](magnet: ClassMagnet[T]): Directive1[T] = + def headerValueByType[T](magnet: HeaderMagnet[T]): Directive1[T] = headerValuePF(magnet.extractPF) | reject(MissingHeaderRejection(magnet.runtimeClass.getSimpleName)) //#optional-header @@ -97,8 +106,11 @@ trait HeaderDirectives { /** * Extract the header value of the optional HTTP request header with the given type. + * + * Custom headers will only be matched by this directive if they extend [[ModeledCustomHeader]] + * and provide a companion extending [[ModeledCustomHeaderCompanion]]. */ - def optionalHeaderValueByType[T <: HttpHeader](magnet: ClassMagnet[T]): Directive1[Option[T]] = + def optionalHeaderValueByType[T <: HttpHeader](magnet: HeaderMagnet[T]): Directive1[Option[T]] = optionalHeaderValuePF(magnet.extractPF) private def optionalValue(lowerCaseName: String): HttpHeader ⇒ Option[String] = { @@ -108,3 +120,40 @@ trait HeaderDirectives { } object HeaderDirectives extends HeaderDirectives + +trait HeaderMagnet[T] { + def classTag: ClassTag[T] + def runtimeClass: Class[T] + + /** + * Returns a partial function that checks if the input value is of runtime type + * T and returns the value if it does. Doesn't take erased information into account. + */ + def extractPF: PartialFunction[HttpHeader, T] +} +object HeaderMagnet extends LowPriorityHeaderMagnetImplicits { + + /** + * If possible we want to apply the special logic for [[ModeledCustomHeader]] to extract custom headers by type, + * otherwise the default `fromUnit` is good enough (for headers that the parser emits in the right type already). + */ + implicit def fromUnitForModeledCustomHeader[T <: ModeledCustomHeader[T], H <: ModeledCustomHeaderCompanion[T]] + (u: Unit)(implicit tag: ClassTag[T], companion: ModeledCustomHeaderCompanion[T]): HeaderMagnet[T] = + new HeaderMagnet[T] { + override def runtimeClass = tag.runtimeClass.asInstanceOf[Class[T]] + override def classTag = tag + override def extractPF = { + case h if h.is(companion.lowercaseName) => companion.apply(h.toString) + } + } + +} + +trait LowPriorityHeaderMagnetImplicits { + implicit def fromUnit[T <: HttpHeader](u: Unit)(implicit tag: ClassTag[T]): HeaderMagnet[T] = + new HeaderMagnet[T] { + val classTag: ClassTag[T] = tag + val runtimeClass: Class[T] = tag.runtimeClass.asInstanceOf[Class[T]] + val extractPF: PartialFunction[Any, T] = { case x: T ⇒ x } + } +} \ No newline at end of file