!htp #15927 port MarshallingDirectives from spray
This commit is contained in:
parent
931e8f9b18
commit
193dda6e01
6 changed files with 237 additions and 13 deletions
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.server
|
||||
package directives
|
||||
|
||||
import scala.xml.NodeSeq
|
||||
import akka.http.marshallers.xml.ScalaXmlSupport
|
||||
import akka.http.unmarshalling._
|
||||
import akka.http.marshalling._
|
||||
import akka.http.model._
|
||||
import akka.http.marshallers.sprayjson.SprayJsonSupport._
|
||||
import MediaTypes._
|
||||
import HttpCharsets._
|
||||
import headers._
|
||||
import spray.json.DefaultJsonProtocol._
|
||||
|
||||
class MarshallingDirectivesSpec extends RoutingSpec {
|
||||
import ScalaXmlSupport._
|
||||
|
||||
private val iso88592 = HttpCharsets.getForKey("iso-8859-2").get
|
||||
implicit val IntUnmarshaller: FromEntityUnmarshaller[Int] =
|
||||
nodeSeqUnmarshaller(ContentTypeRange(`text/xml`, iso88592), `text/html`) map {
|
||||
case NodeSeq.Empty ⇒ throw Unmarshaller.NoContentException
|
||||
case x ⇒ x.text.toInt
|
||||
}
|
||||
|
||||
implicit val IntMarshaller: ToEntityMarshaller[Int] =
|
||||
Marshaller.oneOf(ContentType(`application/xhtml+xml`), ContentType(`text/xml`, `UTF-8`)) { contentType ⇒
|
||||
nodeSeqMarshaller(contentType).wrap(contentType) { (i: Int) ⇒ <int>{ i }</int> }
|
||||
}
|
||||
|
||||
"The 'entityAs' directive" should {
|
||||
"extract an object from the requests entity using the in-scope Unmarshaller" in {
|
||||
Put("/", <p>cool</p>) ~> {
|
||||
entity(as[NodeSeq]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "<p>cool</p>" }
|
||||
}
|
||||
"return a RequestEntityExpectedRejection rejection if the request has no entity" in {
|
||||
Put() ~> {
|
||||
entity(as[Int]) { echoComplete }
|
||||
} ~> check { rejection shouldEqual RequestEntityExpectedRejection }
|
||||
}
|
||||
"return an UnsupportedRequestContentTypeRejection if no matching unmarshaller is in scope" in {
|
||||
Put("/", HttpEntity(`text/css`, "<p>cool</p>")) ~> {
|
||||
entity(as[NodeSeq]) { echoComplete }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`))
|
||||
}
|
||||
Put("/", HttpEntity(ContentType(`text/xml`, `UTF-16`), "<int>26</int>")) ~> {
|
||||
entity(as[Int]) { echoComplete }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(ContentTypeRange(`text/xml`, iso88592), `text/html`))
|
||||
}
|
||||
}
|
||||
"cancel UnsupportedRequestContentTypeRejections if a subsequent `entity` directive succeeds" in {
|
||||
Put("/", HttpEntity(`text/plain`, "yeah")) ~> {
|
||||
entity(as[NodeSeq]) { _ ⇒ completeOk } ~
|
||||
entity(as[String]) { _ ⇒ validate(false, "Problem") { completeOk } }
|
||||
} ~> check { rejection shouldEqual ValidationRejection("Problem") }
|
||||
}
|
||||
"extract an Option[T] from the requests entity using the in-scope Unmarshaller" in {
|
||||
Put("/", <p>cool</p>) ~> {
|
||||
entity(as[Option[NodeSeq]]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "Some(<p>cool</p>)" }
|
||||
}
|
||||
"extract an Option[T] as None if the request has no entity" in {
|
||||
Put() ~> {
|
||||
entity(as[Option[Int]]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "None" }
|
||||
}
|
||||
"return an UnsupportedRequestContentTypeRejection if no matching unmarshaller is in scope (for Option[T]s)" in {
|
||||
Put("/", HttpEntity(`text/css`, "<p>cool</p>")) ~> {
|
||||
entity(as[Option[NodeSeq]]) { echoComplete }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`))
|
||||
}
|
||||
}
|
||||
"properly extract with a super-unmarshaller" in {
|
||||
case class Person(name: String)
|
||||
val jsonUnmarshaller: FromEntityUnmarshaller[Person] = jsonFormat1(Person)
|
||||
val xmlUnmarshaller: FromEntityUnmarshaller[Person] =
|
||||
ScalaXmlSupport.nodeSeqUnmarshaller(`text/xml`).map(seq ⇒ Person(seq.text))
|
||||
|
||||
implicit val unmarshaller = Unmarshaller.firstOf(jsonUnmarshaller, xmlUnmarshaller)
|
||||
|
||||
val route = entity(as[Person]) { echoComplete }
|
||||
|
||||
Put("/", HttpEntity(`text/xml`, "<name>Peter Xml</name>")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Person(Peter Xml)"
|
||||
}
|
||||
Put("/", HttpEntity(`application/json`, """{ "name": "Paul Json" }""")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Person(Paul Json)"
|
||||
}
|
||||
Put("/", HttpEntity(`text/plain`, """name = Sir Text }""")) ~> route ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`application/json`, `text/xml`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The 'completeWith' directive" should {
|
||||
"provide a completion function converting custom objects to an HttpEntity using the in-scope marshaller" in {
|
||||
Get() ~> completeWith(instanceOf[Int]) { prod ⇒ prod(42) } ~> check {
|
||||
responseEntity shouldEqual HttpEntity(ContentType(`application/xhtml+xml`, `UTF-8`), "<int>42</int>")
|
||||
}
|
||||
}
|
||||
"return a UnacceptedResponseContentTypeRejection rejection if no acceptable marshaller is in scope" in {
|
||||
Get() ~> Accept(`text/css`) ~> completeWith(instanceOf[Int]) { prod ⇒ prod(42) } ~> check {
|
||||
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, ContentType(`text/xml`, `UTF-8`)))
|
||||
}
|
||||
}
|
||||
"convert the response content to an accepted charset" in {
|
||||
Get() ~> `Accept-Charset`(`UTF-8`) ~> completeWith(instanceOf[String]) { prod ⇒ prod("Hällö") } ~> check {
|
||||
responseEntity shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), "Hällö")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The 'handleWith' directive" should {
|
||||
def times2(x: Int) = x * 2
|
||||
|
||||
"support proper round-trip content unmarshalling/marshalling to and from a function" in (
|
||||
Put("/", HttpEntity(`text/html`, "<int>42</int>")) ~> Accept(`text/xml`) ~> handleWith(times2)
|
||||
~> check { responseEntity shouldEqual HttpEntity(ContentType(`text/xml`, `UTF-8`), "<int>84</int>") })
|
||||
|
||||
"result in UnsupportedRequestContentTypeRejection rejection if there is no unmarshaller supporting the requests charset" in (
|
||||
Put("/", HttpEntity(`text/xml`, "<int>42</int>")) ~> Accept(`text/xml`) ~> handleWith(times2)
|
||||
~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(ContentTypeRange(`text/xml`, iso88592), `text/html`))
|
||||
})
|
||||
|
||||
"result in an UnacceptedResponseContentTypeRejection rejection if there is no marshaller supporting the requests Accept-Charset header" in (
|
||||
Put("/", HttpEntity(`text/html`, "<int>42</int>")) ~> addHeaders(Accept(`text/xml`), `Accept-Charset`(`UTF-16`)) ~>
|
||||
handleWith(times2) ~> check {
|
||||
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, ContentType(`text/xml`, `UTF-8`)))
|
||||
})
|
||||
}
|
||||
|
||||
"The marshalling infrastructure for JSON" should {
|
||||
import spray.json._
|
||||
case class Foo(name: String)
|
||||
implicit val fooFormat = jsonFormat1(Foo)
|
||||
val foo = Foo("Hällö")
|
||||
|
||||
"render JSON with UTF-8 encoding if no `Accept-Charset` request header is present" in {
|
||||
Get() ~> complete(foo) ~> check {
|
||||
responseEntity shouldEqual HttpEntity(ContentType(`application/json`, `UTF-8`), foo.toJson.prettyPrint)
|
||||
}
|
||||
}
|
||||
"reject JSON rendering if an `Accept-Charset` request header requests a non-UTF-8 encoding" in {
|
||||
Get() ~> `Accept-Charset`(`ISO-8859-1`) ~> complete(foo) ~> check {
|
||||
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(ContentType(`application/json`, `UTF-8`)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ trait Directives extends RouteConcatenation
|
|||
with FutureDirectives
|
||||
with HeaderDirectives
|
||||
with HostDirectives
|
||||
//with MarshallingDirectives
|
||||
with MarshallingDirectives
|
||||
with MethodDirectives
|
||||
with MiscDirectives
|
||||
with ParameterDirectives
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@
|
|||
|
||||
package akka.http.server
|
||||
|
||||
import akka.http.model._
|
||||
import akka.http.model.headers.{ HttpChallenge, ByteRange, HttpEncoding }
|
||||
|
||||
import scala.collection.immutable
|
||||
import akka.http.model._
|
||||
import headers._
|
||||
|
||||
/**
|
||||
* A rejection encapsulates a specific reason why a Route was not able to handle a request. Rejections are gathered
|
||||
|
|
@ -71,7 +70,7 @@ case class MalformedHeaderRejection(headerName: String, errorMsg: String,
|
|||
* Rejection created by unmarshallers.
|
||||
* Signals that the request was rejected because the requests content-type is unsupported.
|
||||
*/
|
||||
case class UnsupportedRequestContentTypeRejection(errorMsg: String) extends Rejection
|
||||
case class UnsupportedRequestContentTypeRejection(supported: Set[ContentTypeRange]) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by decoding filters.
|
||||
|
|
@ -110,7 +109,7 @@ case object RequestEntityExpectedRejection extends Rejection
|
|||
* Signals that the request was rejected because the service is not capable of producing a response entity whose
|
||||
* content type is accepted by the client
|
||||
*/
|
||||
case class UnacceptedResponseContentTypeRejection(supported: Seq[ContentType]) extends Rejection
|
||||
case class UnacceptedResponseContentTypeRejection(supported: Set[ContentType]) extends Rejection
|
||||
|
||||
/**
|
||||
* Rejection created by encoding filters.
|
||||
|
|
|
|||
|
|
@ -100,16 +100,16 @@ object RejectionHandler {
|
|||
complete(NotAcceptable, "Resource representation is only available with these Content-Types:\n" + supported.map(_.value).mkString("\n"))
|
||||
|
||||
case rejections @ (UnacceptedResponseEncodingRejection(_) +: _) ⇒
|
||||
val supported = rejections.collect { case UnacceptedResponseEncodingRejection(supported) ⇒ supported }
|
||||
val supported = rejections.collect { case UnacceptedResponseEncodingRejection(x) ⇒ x }
|
||||
complete(NotAcceptable, "Resource representation is only available with these Content-Encodings:\n" + supported.map(_.value).mkString("\n"))
|
||||
|
||||
case rejections @ (UnsupportedRequestContentTypeRejection(_) +: _) ⇒
|
||||
val supported = rejections.collect { case UnsupportedRequestContentTypeRejection(supported) ⇒ supported }
|
||||
complete(UnsupportedMediaType, "There was a problem with the requests Content-Type:\n" + supported.mkString(" or "))
|
||||
val supported = rejections.collect { case UnsupportedRequestContentTypeRejection(x) ⇒ x }
|
||||
complete(UnsupportedMediaType, "The request's Content-Type is not supported. Expected:\n" + supported.mkString(" or "))
|
||||
|
||||
case rejections @ (UnsupportedRequestEncodingRejection(_) +: _) ⇒
|
||||
val supported = rejections.collect { case UnsupportedRequestEncodingRejection(supported) ⇒ supported }
|
||||
complete(BadRequest, "The requests Content-Encoding must be one the following:\n" + supported.map(_.value).mkString("\n"))
|
||||
val supported = rejections.collect { case UnsupportedRequestEncodingRejection(x) ⇒ x }
|
||||
complete(BadRequest, "The request's Content-Encoding is not supported. Expected:\n" + supported.map(_.value).mkString(" or "))
|
||||
|
||||
case ValidationRejection(msg, _) +: _ ⇒
|
||||
complete(BadRequest, msg)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import akka.stream.FlowMaterializer
|
|||
|
||||
import scala.concurrent.{ Future, ExecutionContext }
|
||||
import akka.event.LoggingAdapter
|
||||
import akka.http.marshalling.ToResponseMarshallable
|
||||
import akka.http.marshalling.{ Marshal, ToResponseMarshallable }
|
||||
import akka.http.util.FastFuture
|
||||
import akka.http.model._
|
||||
import FastFuture._
|
||||
|
|
@ -33,7 +33,11 @@ private[http] class RequestContextImpl(
|
|||
override def complete(trm: ToResponseMarshallable): Future[RouteResult] =
|
||||
trm(request)(executionContext)
|
||||
.fast.map(res ⇒ RouteResult.Complete(res))(executionContext)
|
||||
.fast.recover { case RejectionError(rej) ⇒ RouteResult.Rejected(rej :: Nil) }(executionContext)
|
||||
.fast.recover {
|
||||
case Marshal.UnacceptableResponseContentTypeException(supported) ⇒
|
||||
RouteResult.Rejected(UnacceptedResponseContentTypeRejection(supported) :: Nil)
|
||||
case RejectionError(rej) ⇒ RouteResult.Rejected(rej :: Nil)
|
||||
}(executionContext)
|
||||
|
||||
override def reject(rejections: Rejection*): Future[RouteResult] =
|
||||
FastFuture.successful(RouteResult.Rejected(rejections.toVector))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.server
|
||||
package directives
|
||||
|
||||
import scala.concurrent.Promise
|
||||
import scala.util.{ Failure, Success }
|
||||
import akka.http.marshalling.ToResponseMarshaller
|
||||
import akka.http.unmarshalling.{ Unmarshaller, FromRequestUnmarshaller }
|
||||
import akka.http.util._
|
||||
|
||||
trait MarshallingDirectives {
|
||||
import BasicDirectives._
|
||||
import FutureDirectives._
|
||||
import RouteDirectives._
|
||||
|
||||
/**
|
||||
* Unmarshalls the requests entity to the given type passes it to its inner Route.
|
||||
* If there is a problem with unmarshalling the request is rejected with the [[Rejection]]
|
||||
* produced by the unmarshaller.
|
||||
*/
|
||||
def entity[T](um: FromRequestUnmarshaller[T]): Directive1[T] =
|
||||
extractRequest.flatMap[Tuple1[T]] { request ⇒
|
||||
onComplete(um(request)) flatMap {
|
||||
case Success(value) ⇒ provide(value)
|
||||
case Failure(Unmarshaller.NoContentException) ⇒ reject(RequestEntityExpectedRejection)
|
||||
case Failure(Unmarshaller.UnsupportedContentTypeException(x)) ⇒ reject(UnsupportedRequestContentTypeRejection(x))
|
||||
case Failure(x) ⇒ reject(MalformedRequestContentRejection(x.getMessage.nullAsEmpty, Option(x.getCause)))
|
||||
}
|
||||
} & cancelRejections(RequestEntityExpectedRejection.getClass, classOf[UnsupportedRequestContentTypeRejection])
|
||||
|
||||
/**
|
||||
* Returns the in-scope [[FromRequestUnmarshaller]] for the given type.
|
||||
*/
|
||||
def as[T](implicit um: FromRequestUnmarshaller[T]) = um
|
||||
|
||||
/**
|
||||
* Uses the marshaller for the given type to produce a completion function that is passed to its inner function.
|
||||
* You can use it do decouple marshaller resolution from request completion.
|
||||
*/
|
||||
def completeWith[T](marshaller: ToResponseMarshaller[T])(inner: (T ⇒ Unit) ⇒ Unit): Route = { ctx ⇒
|
||||
import ctx.executionContext
|
||||
implicit val m = marshaller
|
||||
val promise = Promise[T]()
|
||||
inner(promise.success(_))
|
||||
ctx.complete(promise.future)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the in-scope Marshaller for the given type.
|
||||
*/
|
||||
def instanceOf[T](implicit m: ToResponseMarshaller[T]): ToResponseMarshaller[T] = m
|
||||
|
||||
/**
|
||||
* Completes the request using the given function. The input to the function is produced with the in-scope
|
||||
* entity unmarshaller and the result value of the function is marshalled with the in-scope marshaller.
|
||||
*/
|
||||
def handleWith[A, B](f: A ⇒ B)(implicit um: FromRequestUnmarshaller[A], m: ToResponseMarshaller[B]): Route =
|
||||
entity(um) { a ⇒ complete(f(a)) }
|
||||
}
|
||||
|
||||
object MarshallingDirectives extends MarshallingDirectives
|
||||
Loading…
Add table
Add a link
Reference in a new issue