=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.model._
import akka.http.scaladsl.server.MissingHeaderRejection import akka.http.scaladsl.server.MissingHeaderRejection
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.http.scaladsl.server.util.ClassMagnet
import docs.http.scaladsl.server.RoutingSpec import docs.http.scaladsl.server.RoutingSpec
import headers._ import headers._
import StatusCodes._ 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 __ @github@/akka-http-core/src/test/scala/akka/http/impl/engine/rendering/ResponseRendererSpec.scala#L422
.. _custom-headers-scala:
Custom Headers 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 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. 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 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 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. 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 Example
------- -------

View file

@ -87,6 +87,7 @@ abstract class ModeledCustomHeaderCompanion[H <: ModeledCustomHeader[H]] {
case _ None 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 { "create an outgoing connection (with 6 parameters)" in {
val (host, port, binding) = runServer() val (host, port, binding) = runServer()
println("host = " + host)
val flow = http.outgoingConnection( val flow = http.outgoingConnection(
toHost(host, port), toHost(host, port),
Optional.empty(), 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.{ HttpHeader, StatusCodes }
import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.server.directives.HeaderMagnet
import scala.concurrent.Future import scala.concurrent.Future
import scala.util.{ Success, Failure, Try } import scala.util.{ Success, Failure, Try }
@ -13,20 +14,26 @@ import scala.util.{ Success, Failure, Try }
object ModeledCustomHeaderSpec { object ModeledCustomHeaderSpec {
//#modeled-api-key-custom-header //#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] { final class ApiTokenHeader(token: String) extends ModeledCustomHeader[ApiTokenHeader] {
def renderInRequests = false def renderInRequests = false
def renderInResponses = false def renderInResponses = false
override val companion = ApiTokenHeader override val companion = ApiTokenHeader
override def value: String = token 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 //#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] { object DifferentHeader extends ModeledCustomHeaderCompanion[DifferentHeader] {
def renderInRequests = false def renderInRequests = false
def renderInResponses = false def renderInResponses = false
@ -35,12 +42,6 @@ object ModeledCustomHeaderSpec {
if (value contains " ") Failure(new Exception("Contains illegal whitespace!")) if (value contains " ") Failure(new Exception("Contains illegal whitespace!"))
else Success(new DifferentHeader(value)) 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 //#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 { "fail with useful message when unable to parse" in {
val ex = intercept[Exception] { val ex = intercept[Exception] {
DifferentHeader("Hello world") // illegal " " DifferentHeader("Hello world") // illegal " "

View file

@ -7,7 +7,6 @@ package akka.http.javadsl.server
import akka.actor.ActorSystem import akka.actor.ActorSystem
import akka.http.scaladsl.{ server, Http } import akka.http.scaladsl.{ server, Http }
import akka.http.scaladsl.Http.ServerBinding import akka.http.scaladsl.Http.ServerBinding
import akka.http.scaladsl.server.RouteResult
import akka.http.impl.server.RouteImplementation import akka.http.impl.server.RouteImplementation
import akka.stream.{ ActorMaterializer, Materializer } import akka.stream.{ ActorMaterializer, Materializer }
import akka.stream.scaladsl.{ Keep, Sink } import akka.stream.scaladsl.{ Keep, Sink }
@ -39,7 +38,7 @@ trait HttpServiceBase {
import system.dispatcher import system.dispatcher
val r: server.Route = RouteImplementation(route) 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.javadsl.server.RequestVal
import akka.http.scaladsl.model import akka.http.scaladsl.model
import akka.http.scaladsl.server.Directive1 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.compat.java8.OptionConverters._
import scala.reflect.{ ClassTag, classTag } import scala.reflect.{ ClassTag, classTag }
@ -31,7 +31,7 @@ object Headers {
HeaderImpl[HttpHeader](name, _ optionalHeaderInstanceByName(name.toLowerCase()).map(_.asScala), classTag[HttpHeader]) HeaderImpl[HttpHeader](name, _ optionalHeaderInstanceByName(name.toLowerCase()).map(_.asScala), classTag[HttpHeader])
def byClass[T <: HttpHeader](clazz: Class[T]): Header[T] = 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]] = private def optionalHeaderInstanceByName(lowercaseName: String): Directive1[Optional[model.HttpHeader]] =
extract(_.request.headers.collectFirst { extract(_.request.headers.collectFirst {

View file

@ -5,7 +5,13 @@
package akka.http.scaladsl.server package akka.http.scaladsl.server
package directives 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 scala.util.control.NonFatal
import akka.http.javadsl.{ model => jm }
import akka.http.scaladsl.server.util.ClassMagnet import akka.http.scaladsl.server.util.ClassMagnet
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
import akka.http.impl.util._ import akka.http.impl.util._
@ -55,8 +61,11 @@ trait HeaderDirectives {
/** /**
* Extracts the first HTTP request header of the given type. * 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]]. * 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)) headerValuePF(magnet.extractPF) | reject(MissingHeaderRejection(magnet.runtimeClass.getSimpleName))
//#optional-header //#optional-header
@ -97,8 +106,11 @@ trait HeaderDirectives {
/** /**
* Extract the header value of the optional HTTP request header with the given type. * 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) optionalHeaderValuePF(magnet.extractPF)
private def optionalValue(lowerCaseName: String): HttpHeader Option[String] = { private def optionalValue(lowerCaseName: String): HttpHeader Option[String] = {
@ -108,3 +120,40 @@ trait HeaderDirectives {
} }
object HeaderDirectives extends 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 }
}
}