=htp #20052 headerValueByType now works with custom headers

This commit is contained in:
Konrad Malawski 2016-03-16 15:03:47 +01:00
parent f34ef537f7
commit d83a323549
10 changed files with 106 additions and 19 deletions

View file

@ -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._

View file

@ -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
--------------

View file

@ -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
-------

View file

@ -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
-------

View file

@ -87,6 +87,7 @@ abstract class ModeledCustomHeaderCompanion[H <: ModeledCustomHeader[H]] {
case _ None
}
final implicit val implicitlyLocatableCompanion: ModeledCustomHeaderCompanion[H] = this
}
/**

View file

@ -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(),

View file

@ -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 " "

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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 }
}
}